complete refactor
This commit is contained in:
14
Blueberry.Redmine/Blueberry.Redmine.csproj
Normal file
14
Blueberry.Redmine/Blueberry.Redmine.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
95
Blueberry.Redmine/Dto/CustomFieldList.cs
Normal file
95
Blueberry.Redmine/Dto/CustomFieldList.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class CustomFieldList
|
||||
{
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("customized_type")]
|
||||
public string CustomizedType { get; set; }
|
||||
|
||||
[JsonPropertyName("field_format")]
|
||||
public string FieldFormat { get; set; }
|
||||
|
||||
[JsonPropertyName("regexp")]
|
||||
public string Regexp { get; set; }
|
||||
|
||||
[JsonPropertyName("min_length")]
|
||||
public int? MinLength { get; set; }
|
||||
|
||||
[JsonPropertyName("max_length")]
|
||||
public int? MaxLength { get; set; }
|
||||
|
||||
[JsonPropertyName("is_required")]
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
[JsonPropertyName("is_filter")]
|
||||
public bool IsFilter { get; set; }
|
||||
|
||||
[JsonPropertyName("searchable")]
|
||||
public bool Searchable { get; set; }
|
||||
|
||||
[JsonPropertyName("multiple")]
|
||||
public bool Multiple { get; set; }
|
||||
|
||||
[JsonPropertyName("default_value")]
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
[JsonPropertyName("visible")]
|
||||
public bool Visible { get; set; }
|
||||
|
||||
[JsonPropertyName("possible_values")]
|
||||
public List<PossibleValue> PossibleValues { get; set; }
|
||||
|
||||
[JsonPropertyName("trackers")]
|
||||
public List<Tracker> Trackers { get; set; }
|
||||
|
||||
[JsonPropertyName("roles")]
|
||||
public List<Role> Roles { get; set; }
|
||||
}
|
||||
|
||||
public class PossibleValue
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("label")]
|
||||
public string Label { get; set; }
|
||||
}
|
||||
|
||||
public class Role
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
}
|
||||
|
||||
public class Tracker
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
197
Blueberry.Redmine/Dto/DetailedIssue.cs
Normal file
197
Blueberry.Redmine/Dto/DetailedIssue.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class DetailedIssue
|
||||
{
|
||||
public class AssignedTo
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Author
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Detail
|
||||
{
|
||||
[JsonPropertyName("property")]
|
||||
public string Property { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("old_value")]
|
||||
public string OldValue { get; set; }
|
||||
|
||||
[JsonPropertyName("new_value")]
|
||||
public string NewValue { get; set; }
|
||||
}
|
||||
|
||||
public class Issue
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("project")]
|
||||
public Project Project { get; set; }
|
||||
|
||||
[JsonPropertyName("tracker")]
|
||||
public Tracker Tracker { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public Status Status { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public Priority Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("author")]
|
||||
public Author Author { get; set; }
|
||||
|
||||
[JsonPropertyName("assigned_to")]
|
||||
public AssignedTo AssignedTo { get; set; }
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("start_date")]
|
||||
public string StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("due_date")]
|
||||
public object DueDate { get; set; }
|
||||
|
||||
[JsonPropertyName("done_ratio")]
|
||||
public int DoneRatio { get; set; }
|
||||
|
||||
[JsonPropertyName("is_private")]
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
[JsonPropertyName("estimated_hours")]
|
||||
public object EstimatedHours { get; set; }
|
||||
|
||||
[JsonPropertyName("total_estimated_hours")]
|
||||
public object TotalEstimatedHours { get; set; }
|
||||
|
||||
[JsonPropertyName("spent_hours")]
|
||||
public double SpentHours { get; set; }
|
||||
|
||||
[JsonPropertyName("total_spent_hours")]
|
||||
public double TotalSpentHours { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("closed_on")]
|
||||
public object ClosedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("journals")]
|
||||
public List<Journal> Journals { get; set; }
|
||||
}
|
||||
|
||||
public class Journal
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("user")]
|
||||
public User User { get; set; }
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string Notes { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("private_notes")]
|
||||
public bool PrivateNotes { get; set; }
|
||||
|
||||
[JsonPropertyName("details")]
|
||||
public List<Detail> Details { get; set; }
|
||||
}
|
||||
|
||||
public class Priority
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("issue")]
|
||||
public Issue Issue { get; set; }
|
||||
}
|
||||
|
||||
public class Status
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Tracker
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
9
Blueberry.Redmine/Dto/IResponseList.cs
Normal file
9
Blueberry.Redmine/Dto/IResponseList.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public interface IResponseList
|
||||
{
|
||||
public int TotalCount { get; set; }
|
||||
public int Offset { get; set; }
|
||||
public int Limit { get; set; }
|
||||
}
|
||||
}
|
||||
183
Blueberry.Redmine/Dto/IssueList.cs
Normal file
183
Blueberry.Redmine/Dto/IssueList.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class IssueList
|
||||
{
|
||||
public class AssignedTo
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Author
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Issue
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("project")]
|
||||
public Project Project { get; set; }
|
||||
public string ProjectName => Project.Name;
|
||||
|
||||
[JsonPropertyName("tracker")]
|
||||
public Tracker Tracker { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public Status Status { get; set; }
|
||||
public string StatusName => Status.Name;
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public Priority Priority { get; set; }
|
||||
public string PriorityName => Priority.Name;
|
||||
|
||||
[JsonPropertyName("author")]
|
||||
public Author Author { get; set; }
|
||||
|
||||
[JsonPropertyName("assigned_to")]
|
||||
public AssignedTo AssignedTo { get; set; }
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("start_date")]
|
||||
public string StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("due_date")]
|
||||
public string DueDate { get; set; }
|
||||
|
||||
[JsonPropertyName("done_ratio")]
|
||||
public int DoneRatio { get; set; }
|
||||
|
||||
[JsonPropertyName("is_private")]
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
[JsonPropertyName("estimated_hours")]
|
||||
public double? EstimatedHours { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
public string LastUpdate
|
||||
{
|
||||
get
|
||||
{
|
||||
var span = DateTime.Now - UpdatedOn;
|
||||
|
||||
if (span.TotalMinutes < 1) return "épp most";
|
||||
if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes} perce";
|
||||
if (span.TotalHours < 24) return $"{(int)span.TotalHours} órája";
|
||||
if (span.TotalDays < 7) return $"{(int)span.TotalDays} napja";
|
||||
if (span.TotalDays < 30) return $"{(int)(span.TotalDays / 7)} hete";
|
||||
if (span.TotalDays < 365) return $"{(int)(span.TotalDays / 30)} hónapja";
|
||||
|
||||
return $"{(int)(span.TotalDays / 365)} éve";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("closed_on")]
|
||||
public DateTime? ClosedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("parent")]
|
||||
public Parent Parent { get; set; }
|
||||
}
|
||||
|
||||
public class Parent
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class Priority
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class SimpleRoot
|
||||
{
|
||||
[JsonPropertyName("issue")]
|
||||
public Issue Issue { get; set; }
|
||||
}
|
||||
|
||||
public class Root : IResponseList
|
||||
{
|
||||
[JsonPropertyName("issues")]
|
||||
public List<Issue> Issues { get; set; }
|
||||
|
||||
[JsonPropertyName("total_count")]
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
[JsonPropertyName("offset")]
|
||||
public int Offset { get; set; }
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int Limit { get; set; }
|
||||
}
|
||||
|
||||
public class Status
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Tracker
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
131
Blueberry.Redmine/Dto/NewIssue.cs
Normal file
131
Blueberry.Redmine/Dto/NewIssue.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class NewIssue
|
||||
{
|
||||
public class Author
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Issue
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("project")]
|
||||
public Project Project { get; set; }
|
||||
|
||||
[JsonPropertyName("tracker")]
|
||||
public Tracker Tracker { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public Status Status { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public Priority Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("author")]
|
||||
public Author Author { get; set; }
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("start_date")]
|
||||
public string StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("due_date")]
|
||||
public object DueDate { get; set; }
|
||||
|
||||
[JsonPropertyName("done_ratio")]
|
||||
public int DoneRatio { get; set; }
|
||||
|
||||
[JsonPropertyName("is_private")]
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
[JsonPropertyName("estimated_hours")]
|
||||
public double EstimatedHours { get; set; }
|
||||
|
||||
[JsonPropertyName("total_estimated_hours")]
|
||||
public double TotalEstimatedHours { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("closed_on")]
|
||||
public object ClosedOn { get; set; }
|
||||
}
|
||||
|
||||
public class Priority
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("issue")]
|
||||
public Issue Issue { get; set; }
|
||||
}
|
||||
|
||||
public class Status
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Tracker
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
32
Blueberry.Redmine/Dto/PriorityList.cs
Normal file
32
Blueberry.Redmine/Dto/PriorityList.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class PriorityList
|
||||
{
|
||||
public class IssuePriority
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("is_default")]
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public bool Active { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("issue_priorities")]
|
||||
public List<IssuePriority> IssuePriorities { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
83
Blueberry.Redmine/Dto/ProjectList.cs
Normal file
83
Blueberry.Redmine/Dto/ProjectList.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class ProjectList
|
||||
{
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Parent
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("identifier")]
|
||||
public string Identifier { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonPropertyName("is_public")]
|
||||
public bool IsPublic { get; set; }
|
||||
|
||||
[JsonPropertyName("inherit_members")]
|
||||
public bool InheritMembers { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("parent")]
|
||||
public Parent Parent { get; set; }
|
||||
}
|
||||
|
||||
public class Root : IResponseList
|
||||
{
|
||||
[JsonPropertyName("projects")]
|
||||
public List<Project> Projects { get; set; }
|
||||
|
||||
[JsonPropertyName("total_count")]
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
[JsonPropertyName("offset")]
|
||||
public int Offset { get; set; }
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int Limit { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
89
Blueberry.Redmine/Dto/ProjectTrackers.cs
Normal file
89
Blueberry.Redmine/Dto/ProjectTrackers.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class ProjectTrackers
|
||||
{
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Parent
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("identifier")]
|
||||
public string Identifier { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("homepage")]
|
||||
public string Homepage { get; set; }
|
||||
|
||||
[JsonPropertyName("parent")]
|
||||
public Parent Parent { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonPropertyName("is_public")]
|
||||
public bool IsPublic { get; set; }
|
||||
|
||||
[JsonPropertyName("inherit_members")]
|
||||
public bool InheritMembers { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
|
||||
[JsonPropertyName("trackers")]
|
||||
public List<Tracker> Trackers { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("project")]
|
||||
public Project Project { get; set; }
|
||||
}
|
||||
|
||||
public class Tracker
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
29
Blueberry.Redmine/Dto/StatusList.cs
Normal file
29
Blueberry.Redmine/Dto/StatusList.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class StatusList
|
||||
{
|
||||
public class IssueStatus
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("is_closed")]
|
||||
public bool IsClosed { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("issue_statuses")]
|
||||
public List<IssueStatus> IssueStatuses { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
74
Blueberry.Redmine/Dto/UserInfo.cs
Normal file
74
Blueberry.Redmine/Dto/UserInfo.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class UserInfo
|
||||
{
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonPropertyName("user")]
|
||||
public User User { get; set; }
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("login")]
|
||||
public string Login { get; set; }
|
||||
|
||||
[JsonPropertyName("admin")]
|
||||
public bool Admin { get; set; }
|
||||
|
||||
[JsonPropertyName("firstname")]
|
||||
public string Firstname { get; set; }
|
||||
|
||||
[JsonPropertyName("lastname")]
|
||||
public string Lastname { get; set; }
|
||||
|
||||
[JsonPropertyName("mail")]
|
||||
public string Mail { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("last_login_on")]
|
||||
public DateTime LastLoginOn { get; set; }
|
||||
|
||||
[JsonPropertyName("passwd_changed_on")]
|
||||
public DateTime PasswdChangedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("twofa_scheme")]
|
||||
public object TwofaScheme { get; set; }
|
||||
|
||||
[JsonPropertyName("api_key")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
107
Blueberry.Redmine/Dto/UserTime.cs
Normal file
107
Blueberry.Redmine/Dto/UserTime.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Blueberry.Redmine.Dto
|
||||
{
|
||||
public class UserTime
|
||||
{
|
||||
public class Activity
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class CustomField
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class Issue
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class Root : IResponseList
|
||||
{
|
||||
[JsonPropertyName("time_entries")]
|
||||
public List<TimeEntry> TimeEntries { get; set; }
|
||||
|
||||
[JsonPropertyName("total_count")]
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
[JsonPropertyName("offset")]
|
||||
public int Offset { get; set; }
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int Limit { get; set; }
|
||||
}
|
||||
|
||||
public class TimeEntry
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("project")]
|
||||
public Project Project { get; set; }
|
||||
|
||||
[JsonPropertyName("issue")]
|
||||
public Issue Issue { get; set; }
|
||||
|
||||
[JsonPropertyName("user")]
|
||||
public User User { get; set; }
|
||||
|
||||
[JsonPropertyName("activity")]
|
||||
public Activity Activity { get; set; }
|
||||
|
||||
[JsonPropertyName("hours")]
|
||||
public double Hours { get; set; }
|
||||
|
||||
[JsonPropertyName("comments")]
|
||||
public string Comments { get; set; }
|
||||
|
||||
[JsonPropertyName("spent_on")]
|
||||
public string SpentOn { get; set; }
|
||||
|
||||
[JsonPropertyName("created_on")]
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_on")]
|
||||
public DateTime UpdatedOn { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_fields")]
|
||||
public List<CustomField> CustomFields { get; set; }
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
282
Blueberry.Redmine/RedmineApiClient.cs
Normal file
282
Blueberry.Redmine/RedmineApiClient.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using Blueberry.Redmine.Dto;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static Blueberry.Redmine.Dto.UserTime;
|
||||
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
public class RedmineApiClient
|
||||
{
|
||||
private const int RETRY_DELAY_MS = 2000;
|
||||
private const int PAGING_LIMIT = 50;
|
||||
private readonly RedmineConfig _config;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
|
||||
private readonly ILogger<RedmineApiClient> _logger;
|
||||
readonly HttpClient _httpClient;
|
||||
|
||||
public RedmineApiClient(RedmineConfig config, ILogger<RedmineApiClient> logger, HttpClient httpClient)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
private async Task<TResponse?> SendRequestAsync<TResponse>(HttpMethod method, string endpoint, object? payload = null, CancellationToken? token = null)
|
||||
{
|
||||
string url = $"{_config.RedmineUrl}/{endpoint}";
|
||||
int maxRetries = _config.MaxRetries;
|
||||
CancellationToken cancellationToken = token ?? CancellationToken.None;
|
||||
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
using var request = new HttpRequestMessage(method, url);
|
||||
request.Headers.Add("X-Redmine-API-Key", _config.ApiKey);
|
||||
|
||||
if (payload != null)
|
||||
{
|
||||
string json = JsonSerializer.Serialize(payload, _jsonOptions);
|
||||
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Request cancelled by token");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
if (response.Content.Headers.ContentLength == 0)
|
||||
return default;
|
||||
|
||||
var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<TResponse>(responseStream, _jsonOptions);
|
||||
}
|
||||
|
||||
bool isServerError = (int)response.StatusCode >= 500 && (int)response.StatusCode < 600;
|
||||
|
||||
if (isServerError && attempt < maxRetries)
|
||||
{
|
||||
_logger.LogWarning("Received {StatusCode} from Redmine. Retrying in {Delay}ms (Attempt {Attempt} of {MaxRetries})", response.StatusCode, RETRY_DELAY_MS, attempt + 1, maxRetries);
|
||||
|
||||
response.Dispose();
|
||||
|
||||
await Task.Delay(RETRY_DELAY_MS).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
string errorBody = await response.Content.ReadAsStringAsync();
|
||||
response.Dispose();
|
||||
|
||||
_logger.LogError("Error ({StatusCode}): {ErrorBody}", response.StatusCode, errorBody);
|
||||
throw new RedmineApiException($"Redmine API Error {response.StatusCode}: {errorBody}", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
throw new RedmineApiException("Redmine API Unreachable");
|
||||
}
|
||||
|
||||
private async Task<List<TReturn>> SendRequestWithPagingAsync<TResponse, TReturn>(HttpMethod method, string endpoint, int limit, Func<TResponse, List<TReturn>> itemParser,
|
||||
IProgress<(int current, int total)>? progress = null, object? payload = null, CancellationToken? token = null) where TResponse : IResponseList
|
||||
{
|
||||
var offset = 0;
|
||||
|
||||
List<TReturn> returnList = [];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var path = $"{endpoint}&limit={limit}&offset={offset}";
|
||||
|
||||
var responseList = await SendRequestAsync<TResponse>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
returnList.AddRange(itemParser(responseList));
|
||||
|
||||
if (offset + limit >= responseList.TotalCount)
|
||||
break;
|
||||
|
||||
offset += limit;
|
||||
if(progress != null)
|
||||
{
|
||||
var current = Math.Min(offset + limit, responseList.TotalCount);
|
||||
progress.Report((current, responseList.TotalCount));
|
||||
}
|
||||
}
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
public async Task<List<StatusList.IssueStatus>> GetStatusesAsync(CancellationToken? token = null)
|
||||
{
|
||||
var path = "issue_statuses.json";
|
||||
|
||||
var statusList = await SendRequestAsync<StatusList.Root>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return statusList.IssueStatuses;
|
||||
}
|
||||
|
||||
public async Task<List<CustomFieldList.CustomField>> GetCustomFieldsAsync(CancellationToken? token = null)
|
||||
{
|
||||
var path = "custom_fields.json";
|
||||
|
||||
var fields = await SendRequestAsync<CustomFieldList.Root>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return fields.CustomFields;
|
||||
}
|
||||
|
||||
public async Task<List<PriorityList.IssuePriority>> GetPrioritiesAsync(CancellationToken? token = null)
|
||||
{
|
||||
var path = "enumerations/issue_priorities.json";
|
||||
|
||||
var fields = await SendRequestAsync<PriorityList.Root>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return fields.IssuePriorities;
|
||||
}
|
||||
|
||||
public async Task<List<IssueList.Issue>> GetOpenIssuesByAssignee(int userId, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken ? token = null)
|
||||
{
|
||||
var path = $"issues.json?assigned_to_id={userId}&status_id=open";
|
||||
|
||||
var items = await SendRequestWithPagingAsync<IssueList.Root, IssueList.Issue>(HttpMethod.Get, path, limit, (x) => x.Issues,
|
||||
progress, token: token);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<List<ProjectList.Project>> GetProjects(int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
var path = $"projects.json";
|
||||
|
||||
var items = await SendRequestWithPagingAsync<ProjectList.Root, ProjectList.Project>(HttpMethod.Get, path, limit, (x) => x.Projects,
|
||||
progress, token: token);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<List<ProjectTrackers.Tracker>> GetTrackersForProject(string projectId, CancellationToken? token = null)
|
||||
{
|
||||
var path = $"projects/{projectId}.json?include=trackers";
|
||||
|
||||
var trackers = await SendRequestAsync<ProjectTrackers.Root>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return trackers.Project.Trackers;
|
||||
}
|
||||
|
||||
public async Task<double> GetTotalTimeForUser(int userId, DateTime start, DateTime end, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
var sText = start.ToString("yyyy-MM-dd");
|
||||
var eText = end.ToString("yyyy-MM-dd");
|
||||
|
||||
var path = $"time_entries.json?from={sText}&to={eText}&user_id={userId}";
|
||||
|
||||
|
||||
var timedata = await SendRequestWithPagingAsync<UserTime.Root, UserTime.TimeEntry>(HttpMethod.Get, path, limit, (x)=> x.TimeEntries, progress, token: token);
|
||||
|
||||
var hours = timedata.Sum(x => x.Hours);
|
||||
|
||||
return hours;
|
||||
}
|
||||
|
||||
public async Task<DetailedIssue.Issue> GetIssue(int issueId, CancellationToken? token = null)
|
||||
{
|
||||
var path = $"issues/{issueId}.json?include=journals";
|
||||
|
||||
var issue = await SendRequestAsync<DetailedIssue.Root>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return issue.Issue;
|
||||
}
|
||||
|
||||
public async Task<IssueList.Issue> GetSimpleIssue(int issueId, CancellationToken? token = null)
|
||||
{
|
||||
var path = $"issues/{issueId}.json?include=journals";
|
||||
|
||||
var issue = await SendRequestAsync<IssueList.SimpleRoot>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return issue.Issue;
|
||||
}
|
||||
|
||||
public async Task<UserInfo.User> GetUserAsync(int? userId = null, CancellationToken? token = null)
|
||||
{
|
||||
var path = "users/current.json";
|
||||
|
||||
if (userId != null)
|
||||
path = $"users/{userId}.json";
|
||||
|
||||
var user = await SendRequestAsync<UserInfo.Root>(HttpMethod.Get, path, token: token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return user.User;
|
||||
}
|
||||
|
||||
public async Task SetIssueStatus(int issueId, int statusId, CancellationToken? token = null)
|
||||
{
|
||||
var path = $"issues/{issueId}.json";
|
||||
|
||||
var payload = new
|
||||
{
|
||||
issue = new
|
||||
{
|
||||
status_id = statusId
|
||||
}
|
||||
};
|
||||
|
||||
await SendRequestAsync<object>(HttpMethod.Put, path, payload, token: token);
|
||||
}
|
||||
|
||||
public async Task<int> CreateNewIssue(int projectId, int trackerId, string subject, string description,
|
||||
double estimatedHours, int priorityId, int? assigneeId = null, int? parentIssueId = null, CancellationToken? token = null)
|
||||
{
|
||||
var path = "issues.json";
|
||||
|
||||
var payload = new
|
||||
{
|
||||
project_id = projectId,
|
||||
tracker_id = trackerId,
|
||||
subject = subject,
|
||||
description = description,
|
||||
priority_id = priorityId,
|
||||
assigned_to_id = assigneeId,
|
||||
parent_issue_id = parentIssueId,
|
||||
custom_fields = new // TODO, do something about this
|
||||
{
|
||||
id = 65,
|
||||
value = estimatedHours
|
||||
}
|
||||
};
|
||||
|
||||
var issue = await SendRequestAsync<NewIssue.Root>(HttpMethod.Post, path, payload, token)
|
||||
?? throw new NullReferenceException();
|
||||
|
||||
return issue.Issue.Id;
|
||||
}
|
||||
|
||||
public async Task LogTimeAsync(int issueId, double hours, string comments, DateTime? date = null, int? activityId = null, CancellationToken? token = null)
|
||||
{
|
||||
string url = $"time_entries.json";
|
||||
string dateString = (date ?? DateTime.Now).ToString("yyyy-MM-dd");
|
||||
|
||||
var payload = new
|
||||
{
|
||||
time_entry = new
|
||||
{
|
||||
issue_id = issueId,
|
||||
hours = hours,
|
||||
comments = comments,
|
||||
spent_on = dateString,
|
||||
activity_id = activityId
|
||||
}
|
||||
};
|
||||
|
||||
await SendRequestAsync<object>(HttpMethod.Post, url, payload, token: token);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Blueberry.Redmine/RedmineApiException.cs
Normal file
17
Blueberry.Redmine/RedmineApiException.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
public class RedmineApiException : Exception
|
||||
{
|
||||
public int? StatusCode { get; }
|
||||
|
||||
public RedmineApiException(string message, int? statusCode = null) : base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public RedmineApiException(string message, Exception innerException, int? statusCode = null) : base(message, innerException)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
208
Blueberry.Redmine/RedmineCache.cs
Normal file
208
Blueberry.Redmine/RedmineCache.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
class RedmineCache<T>
|
||||
{
|
||||
private List<T> Items { get; set; } = [];
|
||||
|
||||
public DateTime LastUpdated { get; set; } = DateTime.MinValue;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TimeSpan _cacheDuration;
|
||||
private readonly object _lock = new();
|
||||
private int _maxCapacity = int.MaxValue;
|
||||
private bool _useSlidingExpiration = false;
|
||||
private DateTime _lastAccessed = DateTime.MinValue;
|
||||
private string? _cacheFilePath;
|
||||
private Func<Task<List<T>>>? _refreshCallback;
|
||||
|
||||
private class CacheData
|
||||
{
|
||||
public List<T> Items { get; set; } = [];
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public DateTime LastAccessed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RedmineCache class.
|
||||
/// </summary>
|
||||
/// <param name="cacheDuration">The time span for which the cache remains valid. After this duration, the cache is considered expired.</param>
|
||||
/// <param name="logger">The logger instance for logging cache operations.</param>
|
||||
/// <param name="maxCapacity">The maximum number of items to store in the cache. Defaults to int.MaxValue (unlimited).</param>
|
||||
/// <param name="useSlidingExpiration">If true, the cache expiration resets on each access (sliding expiration). If false, uses absolute expiration from the last update. Defaults to false.</param>
|
||||
/// <param name="cacheFilePath">Optional file path for persisting the cache to disk. If provided, the cache loads from and saves to this file. Defaults to null (no persistence).</param>
|
||||
/// <param name="refreshCallback">Optional asynchronous callback function to refresh the cache when it expires. Called automatically in GetItemsAsync if the cache is invalid. Defaults to null.</param>
|
||||
public RedmineCache(TimeSpan cacheDuration, ILogger<RedmineCache<T>> logger, int maxCapacity = int.MaxValue, bool useSlidingExpiration = false,
|
||||
string? cacheFilePath = null, Func<Task<List<T>>>? refreshCallback = null)
|
||||
{
|
||||
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
||||
if (cacheDuration <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(cacheDuration));
|
||||
|
||||
_logger = logger;
|
||||
_cacheDuration = cacheDuration;
|
||||
_maxCapacity = maxCapacity;
|
||||
_useSlidingExpiration = useSlidingExpiration;
|
||||
_cacheFilePath = cacheFilePath;
|
||||
_refreshCallback = refreshCallback;
|
||||
|
||||
LoadFromFile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RedmineCache class with cache duration in seconds.
|
||||
/// </summary>
|
||||
/// <param name="cacheDurationSec">The cache duration in seconds. Converted to a TimeSpan internally.</param>
|
||||
/// <param name="logger">The logger instance for logging cache operations.</param>
|
||||
/// <param name="maxCapacity">The maximum number of items to store in the cache. Defaults to int.MaxValue (unlimited).</param>
|
||||
/// <param name="useSlidingExpiration">If true, the cache expiration resets on each access (sliding expiration). If false, uses absolute expiration from the last update. Defaults to false.</param>
|
||||
/// <param name="cacheFilePath">Optional file path for persisting the cache to disk. If provided, the cache loads from and saves to this file. Defaults to null (no persistence).</param>
|
||||
/// <param name="refreshCallback">Optional asynchronous callback function to refresh the cache when it expires. Called automatically in GetItemsAsync if the cache is invalid. Defaults to null.</param>
|
||||
public RedmineCache(int cacheDurationSec, ILogger<RedmineCache<T>> logger, int maxCapacity = int.MaxValue, bool useSlidingExpiration = false,
|
||||
string? cacheFilePath = null, Func<Task<List<T>>>? refreshCallback = null)
|
||||
: this(new TimeSpan(0, 0, cacheDurationSec), logger, maxCapacity, useSlidingExpiration, cacheFilePath, refreshCallback) { }
|
||||
|
||||
private void LoadFromFile()
|
||||
{
|
||||
if (_cacheFilePath == null || !File.Exists(_cacheFilePath)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_cacheFilePath);
|
||||
var data = JsonSerializer.Deserialize<CacheData>(json);
|
||||
if (data != null)
|
||||
{
|
||||
Items = data.Items ?? [];
|
||||
LastUpdated = data.LastUpdated;
|
||||
_lastAccessed = data.LastAccessed;
|
||||
}
|
||||
_logger.LogDebug("Loaded cache from {path}", _cacheFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load cache from {path}", _cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveToFile()
|
||||
{
|
||||
if (_cacheFilePath == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var data = new CacheData { Items = Items, LastUpdated = LastUpdated, LastAccessed = _lastAccessed };
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var dir = Path.GetDirectoryName(_cacheFilePath) ?? throw new NullReferenceException();
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(_cacheFilePath, json);
|
||||
_logger.LogDebug("Saved cache to {path}", _cacheFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to save cache to {path}", _cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCache(List<T> newItems)
|
||||
{
|
||||
_logger.LogDebug("Refreshing cache with {count} items", newItems.Count);
|
||||
lock (_lock)
|
||||
{
|
||||
Items = newItems.Count > _maxCapacity ? newItems.Take(_maxCapacity).ToList() : newItems;
|
||||
LastUpdated = DateTime.UtcNow;
|
||||
_lastAccessed = DateTime.UtcNow;
|
||||
SaveToFile();
|
||||
_logger.LogDebug("Cache refreshed");
|
||||
}
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
{
|
||||
_logger.LogDebug("Invalidating cache");
|
||||
lock (_lock)
|
||||
{
|
||||
LastUpdated = DateTime.MinValue;
|
||||
_lastAccessed = DateTime.MinValue;
|
||||
Items.Clear();
|
||||
SaveToFile();
|
||||
_logger.LogDebug("Cache invalidated");
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCacheValid()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
bool valid = !_useSlidingExpiration ? DateTime.UtcNow - LastUpdated <= _cacheDuration : DateTime.UtcNow - _lastAccessed <= _cacheDuration;
|
||||
_logger.LogDebug("Cache valid: {valid}", valid);
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<T> GetItems()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastAccessed = DateTime.UtcNow;
|
||||
_logger.LogDebug("Returning {count} cached items", Items.Count);
|
||||
return Items.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public Task RefreshCacheAsync(List<T> newItems) => Task.Run(() => RefreshCache(newItems));
|
||||
|
||||
public async Task<IReadOnlyList<T>> GetItemsAsync()
|
||||
{
|
||||
bool needsRefresh = false;
|
||||
lock (_lock)
|
||||
{
|
||||
_lastAccessed = DateTime.UtcNow;
|
||||
if (!IsCacheValid() && _refreshCallback != null)
|
||||
{
|
||||
needsRefresh = true;
|
||||
}
|
||||
}
|
||||
if (needsRefresh)
|
||||
{
|
||||
await TryRefreshAsync();
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
_logger.LogDebug("Returning {count} cached items", Items.Count);
|
||||
return Items.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEmpty()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Items.Count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetCount()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Items.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TryRefreshAsync()
|
||||
{
|
||||
if (_refreshCallback != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newItems = await _refreshCallback();
|
||||
RefreshCache(newItems);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh cache via callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Blueberry.Redmine/RedmineConfig.cs
Normal file
14
Blueberry.Redmine/RedmineConfig.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
public class RedmineConfig
|
||||
{
|
||||
public string RedmineUrl { get; set; } = "http://redmine.example.com";
|
||||
public string ApiKey { get; set; } = "";
|
||||
public TimeSpan ProjectCacheDuration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
public TimeSpan IssueCacheDuration { get; set; } = TimeSpan.FromMinutes(5);
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
public int ConcurrencyLimit { get; set; } = 10;
|
||||
public string CacheFilePath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Blueberry", "Cache", "Redmine");
|
||||
public bool IsInitiating { get; set; } = false;
|
||||
}
|
||||
}
|
||||
167
Blueberry.Redmine/RedmineManager.cs
Normal file
167
Blueberry.Redmine/RedmineManager.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using Blueberry.Redmine.Dto;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
public class RedmineManager
|
||||
{
|
||||
private readonly TimeSpan DEFAULT_CACHE_DURATION = TimeSpan.FromHours(1);
|
||||
private readonly RedmineConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly RedmineApiClient _apiClient;
|
||||
private readonly RedmineCache<StatusList.IssueStatus> _statusCache;
|
||||
private readonly RedmineCache<PriorityList.IssuePriority> _priorityCache;
|
||||
private readonly RedmineCache<CustomFieldList.CustomField> _customFieldCache;
|
||||
private readonly RedmineCache<ProjectList.Project> _projectCache;
|
||||
|
||||
public RedmineManager(RedmineConfig config, HttpClient client, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_config = config;
|
||||
_apiClient = new RedmineApiClient(config, loggerFactory.CreateLogger<RedmineApiClient>(), client);
|
||||
|
||||
_statusCache = new RedmineCache<StatusList.IssueStatus>(
|
||||
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<StatusList.IssueStatus>>(), cacheFilePath: $"{_config.CacheFilePath}Statuses.json");
|
||||
|
||||
_priorityCache = new RedmineCache<PriorityList.IssuePriority>(
|
||||
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<PriorityList.IssuePriority>>(), cacheFilePath: $"{_config.CacheFilePath}Priorities.json");
|
||||
|
||||
_customFieldCache = new RedmineCache<CustomFieldList.CustomField>(
|
||||
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<CustomFieldList.CustomField>>(), cacheFilePath: $"{_config.CacheFilePath}CustomFields.json");
|
||||
|
||||
_projectCache = new RedmineCache<ProjectList.Project>(
|
||||
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<ProjectList.Project>>(), cacheFilePath: $"{_config.CacheFilePath}Projects.json");
|
||||
|
||||
_logger = loggerFactory.CreateLogger<RedmineManager>();
|
||||
_logger.LogDebug("Initialized caches");
|
||||
}
|
||||
|
||||
public async Task<bool> IsRedmineAvailable(CancellationToken? token = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _apiClient.GetUserAsync();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<StatusList.IssueStatus>> GetStatusesAsync(CancellationToken? token = null)
|
||||
{
|
||||
if (_statusCache.IsCacheValid())
|
||||
{
|
||||
return await _statusCache.GetItemsAsync();
|
||||
}
|
||||
var statuses = await _apiClient.GetStatusesAsync(token);
|
||||
await _statusCache.RefreshCacheAsync(statuses);
|
||||
return statuses;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PriorityList.IssuePriority>> GetPrioritiesAsync(CancellationToken? token = null)
|
||||
{
|
||||
if (_priorityCache.IsCacheValid())
|
||||
{
|
||||
return await _priorityCache.GetItemsAsync();
|
||||
}
|
||||
var priorities = await _apiClient.GetPrioritiesAsync(token);
|
||||
await _priorityCache.RefreshCacheAsync(priorities);
|
||||
return priorities;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CustomFieldList.CustomField>> GetCustomFieldsAsync(CancellationToken? token = null)
|
||||
{
|
||||
if (_customFieldCache.IsCacheValid())
|
||||
{
|
||||
return await _customFieldCache.GetItemsAsync();
|
||||
}
|
||||
var fields = await _apiClient.GetCustomFieldsAsync(token);
|
||||
await _customFieldCache.RefreshCacheAsync(fields);
|
||||
return fields;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ProjectList.Project>> GetProjectsAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
if (_projectCache.IsCacheValid())
|
||||
{
|
||||
return await _projectCache.GetItemsAsync();
|
||||
}
|
||||
var projects = await _apiClient.GetProjects(limit, progress, token);
|
||||
await _projectCache.RefreshCacheAsync(projects);
|
||||
return projects;
|
||||
}
|
||||
|
||||
public async Task<UserInfo.User> GetCurrentUserAsync(CancellationToken? token = null)
|
||||
{
|
||||
var user = await _apiClient.GetUserAsync(token: token);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<UserInfo.User> GetUserAsync(int userId, CancellationToken? token = null)
|
||||
{
|
||||
var user = await _apiClient.GetUserAsync(userId, token: token);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<List<IssueList.Issue>> GetCurrentUserIssuesAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
var user = await GetCurrentUserAsync(token);
|
||||
return await _apiClient.GetOpenIssuesByAssignee(user.Id, limit, progress, token);
|
||||
}
|
||||
|
||||
public async Task<double> GetCurrentUserTimeAsync(DateTime start, DateTime end, int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
var user = await GetCurrentUserAsync(token);
|
||||
return await _apiClient.GetTotalTimeForUser(user.Id, start, end, limit, progress, token);
|
||||
}
|
||||
|
||||
public async Task<DetailedIssue.Issue> GetIssueAsync(int issueId, CancellationToken? token = null)
|
||||
{
|
||||
return await _apiClient.GetIssue(issueId, token);
|
||||
}
|
||||
|
||||
public async Task<IssueList.Issue> GetSimpleIssueAsync(int issueId, CancellationToken? token = null)
|
||||
{
|
||||
return await _apiClient.GetSimpleIssue(issueId, token);
|
||||
}
|
||||
|
||||
public async Task<List<ProjectTrackers.Tracker>> GetProjectTrackersAsync(int projectId, CancellationToken? token = null)
|
||||
{
|
||||
return await _apiClient.GetTrackersForProject(projectId.ToString(), token);
|
||||
}
|
||||
|
||||
public async Task SetIssueStatusAsync(int issueId, int statusId, CancellationToken? token = null)
|
||||
{
|
||||
await _apiClient.SetIssueStatus(issueId, statusId, token);
|
||||
}
|
||||
|
||||
public async Task LogTimeAsync(int issueId, double hours, string comments, DateTime? date = null, int? activityId = null, CancellationToken? token = null)
|
||||
{
|
||||
await _apiClient.LogTimeAsync(issueId, hours, comments, date, activityId, token);
|
||||
}
|
||||
|
||||
public async Task<int> CreateIssueAsync(int projectId, int trackerId, string subject, string description,
|
||||
double estimatedHours, int priorityId, int? assigneeId = null, int? parentIssueId = null, CancellationToken? token = null)
|
||||
{
|
||||
return await _apiClient.CreateNewIssue(projectId, trackerId, subject, description, estimatedHours, priorityId, assigneeId, parentIssueId, token);
|
||||
}
|
||||
|
||||
public async Task<double> GetCurrentUserTimeTodayAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
return await GetCurrentUserTimeAsync(DateTime.Today, DateTime.Today, limit, progress, token);
|
||||
}
|
||||
|
||||
public async Task<double> GetCurrentUserTimeYesterdayAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
return await GetCurrentUserTimeAsync(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(-1), limit, progress, token);
|
||||
}
|
||||
|
||||
public async Task<double> GetCurrentUserTimeThisMonthAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||
{
|
||||
var start = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
|
||||
var end = start.AddMonths(1).AddDays(-1);
|
||||
return await GetCurrentUserTimeAsync(start, end, limit, progress, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Blueberry.Redmine/RedmineSettingsManager.cs
Normal file
84
Blueberry.Redmine/RedmineSettingsManager.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Text;
|
||||
using System.Security.Cryptography; // For encryption
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
public class RedmineSettingsManager
|
||||
{
|
||||
private readonly string _filePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Blueberry",
|
||||
"settings.json");
|
||||
|
||||
public RedmineConfig Load()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
return new RedmineConfig()
|
||||
{
|
||||
IsInitiating = true,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_filePath);
|
||||
var config = JsonSerializer.Deserialize<RedmineConfig>(json);
|
||||
if(config == null)
|
||||
return new RedmineConfig();
|
||||
|
||||
if (!string.IsNullOrEmpty(config.ApiKey))
|
||||
{
|
||||
config.ApiKey = Unprotect(config.ApiKey);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new RedmineConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(RedmineConfig config)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath) ?? throw new NullReferenceException("Config directory path creation failed."));
|
||||
|
||||
var copy = new RedmineConfig
|
||||
{
|
||||
RedmineUrl = config.RedmineUrl,
|
||||
ApiKey = Protect(config.ApiKey),
|
||||
ProjectCacheDuration = config.ProjectCacheDuration,
|
||||
CacheFilePath = config.CacheFilePath,
|
||||
ConcurrencyLimit = config.ConcurrencyLimit,
|
||||
IssueCacheDuration = config.IssueCacheDuration,
|
||||
MaxRetries = config.MaxRetries,
|
||||
IsInitiating = false
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(copy, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_filePath, json);
|
||||
}
|
||||
|
||||
private string Protect(string clearText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clearText)) return "";
|
||||
byte[] clearBytes = Encoding.UTF8.GetBytes(clearText);
|
||||
byte[] encryptedBytes = ProtectedData.Protect(clearBytes, null, DataProtectionScope.CurrentUser);
|
||||
return Convert.ToBase64String(encryptedBytes);
|
||||
}
|
||||
|
||||
private string Unprotect(string encryptedText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(encryptedText)) return "";
|
||||
try
|
||||
{
|
||||
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
|
||||
byte[] clearBytes = ProtectedData.Unprotect(encryptedBytes, null, DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(clearBytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ""; // Decryption failed (maybe different user/machine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user