diff --git a/Data/Entities/JiraIssue.cs b/Data/Entities/JiraIssue.cs index e3d4072..7e1ddea 100644 --- a/Data/Entities/JiraIssue.cs +++ b/Data/Entities/JiraIssue.cs @@ -17,4 +17,19 @@ public class JiraIssue public DateTimeOffset Published { get; set; } public string? ObjectId { get; set; } public int NeedNotify { get; set; } + + public JiraIssue() { } + protected JiraIssue(JiraIssue issue) + { + this.Key = issue.Key; + this.Summary = issue.Summary; + this.Parent = issue.Parent; + this.Type = issue.Type; + this.Status = issue.Status; + this.Assignee = issue.Assignee; + this.Manager = issue.Manager; + this.Published = issue.Published; + this.ObjectId = issue.ObjectId; + this.NeedNotify = issue.NeedNotify; + } } \ No newline at end of file diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs index fd39bd4..76940d9 100644 --- a/Models/AppSettings.cs +++ b/Models/AppSettings.cs @@ -15,9 +15,9 @@ public class Trilium { public string EtapiToken { get; set; } = string.Empty; public string EtapiBaseUrl { get; set; } = "https://localhost/etapi"; - public string JiraOpenedIssuePageId = string.Empty; - public string JiraWorkingIssuePageId = string.Empty; - public string JiraResolvedIssuePageId = string.Empty; + public string JiraOpenedIssuePageId { get; set; } = string.Empty; + public string JiraWorkingIssuePageId { get; set; } = string.Empty; + public string JiraResolvedIssuePageId { get; set; } = string.Empty; public string DividendPageId { get; set; } = String.Empty; } diff --git a/Models/TriliumNote.cs b/Models/TriliumNote.cs new file mode 100644 index 0000000..54f9772 --- /dev/null +++ b/Models/TriliumNote.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using TriliumMind.Data.Entities; + +namespace TriliumMind.Models; + +public class TriliumNote : JiraIssue +{ + public string ParentNoteId { get; set; } = string.Empty; + public TriliumResponse? TriliumNoteData { get; set; } = null; + + public TriliumNote(JiraIssue issue, string parentNoteId) : base(issue) + { + this.ParentNoteId = parentNoteId; + } + + public StringContent ToNoteContent() + { + var noteData = new + { + parentNoteId = this.ParentNoteId, + title = $"{this.Key} {this.Summary}", + type = "text", + content = "" + }; + + var jsonContent = JsonSerializer.Serialize(noteData); + return new StringContent(jsonContent, null, "application/json"); + } +} diff --git a/Models/TriliumResponse.cs b/Models/TriliumResponse.cs new file mode 100644 index 0000000..7df9789 --- /dev/null +++ b/Models/TriliumResponse.cs @@ -0,0 +1,48 @@ + +public class TriliumResponse +{ + public Note note { get; set; } + public Branch branch { get; set; } +} + +public class Note +{ + public string noteId { get; set; } + public bool isProtected { get; set; } + public string title { get; set; } + public string type { get; set; } + public string mime { get; set; } + public string blobId { get; set; } + public string dateCreated { get; set; } + public string dateModified { get; set; } + public string utcDateCreated { get; set; } + public string utcDateModified { get; set; } + public string[] parentNoteIds { get; set; } + public object[] childNoteIds { get; set; } + public string[] parentBranchIds { get; set; } + public object[] childBranchIds { get; set; } + public Attribute[] attributes { get; set; } +} + +public class Attribute +{ + public string attributeId { get; set; } + public string noteId { get; set; } + public string type { get; set; } + public string name { get; set; } + public string value { get; set; } + public int position { get; set; } + public bool isInheritable { get; set; } + public string utcDateModified { get; set; } +} + +public class Branch +{ + public string branchId { get; set; } + public string noteId { get; set; } + public string parentNoteId { get; set; } + public object prefix { get; set; } + public int notePosition { get; set; } + public bool isExpanded { get; set; } + public string utcDateModified { get; set; } +} diff --git a/Services/Mappers/JiraIssueMapper.cs b/Services/Mappers/JiraIssueMapper.cs index 0b22596..7014132 100644 --- a/Services/Mappers/JiraIssueMapper.cs +++ b/Services/Mappers/JiraIssueMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection.Metadata; using TriliumMind.Data.Entities; using TriliumMind.Models; @@ -26,7 +27,6 @@ public static class JiraIssueMapper NeedNotify = 0 }; } - private static string CleanDisplayName(string? displayName) { if (string.IsNullOrWhiteSpace(displayName)) diff --git a/Services/TriliumService.cs b/Services/TriliumService.cs index 6e9e4de..aa73c9b 100644 --- a/Services/TriliumService.cs +++ b/Services/TriliumService.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; +using TriliumMind.Data.Entities; using TriliumMind.Models; namespace TriliumMind.Services; @@ -18,7 +19,73 @@ public class TriliumService _log = Log.ForContext(); _configs = configs; } - public async Task FettchPageContentsAsync(Issue issue) + + public async Task> PublishNotesAsync(IEnumerable jiraIssues) + { + var issuesToPatch = new List(); + var issuesToPost = new List(); + foreach (var jiraIssue in jiraIssues) + { + if(jiraIssue.Published > DateTimeOffset.MinValue) + issuesToPatch.Add(jiraIssue); + else + issuesToPost.Add(jiraIssue); + break; + } + + // Post note does not support attribute, so additional work is required. + var postedNotes = await PostNotesAsync(issuesToPost); + //issuesToPatch.AddRange(postedIssues); + //var patchedIssues = await PatchNotesAsync(issuesToPatch); + + return issuesToPatch; + } + + public async Task> PostNotesAsync(IEnumerable jiraIssues) + { + var baseUrl = _configs.AppSettings.Trilium.EtapiBaseUrl; + var token = _configs.AppSettings.Trilium.EtapiToken; + var apiUrl = $"{baseUrl}/create-note"; + + var triliumNotes = new List(); + var lastRequestTime = DateTimeOffset.MinValue; + foreach (var jiraIssue in jiraIssues) + { + var timeSinceLastRequest = DateTimeOffset.UtcNow - lastRequestTime; + if (timeSinceLastRequest < TimeSpan.FromSeconds(1)) + { + var delayTime = TimeSpan.FromSeconds(1) - timeSinceLastRequest; + await Task.Delay(delayTime); + } + + var note = new TriliumNote(jiraIssue, GetParentPageId(jiraIssue.Status)); + var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); + request.Headers.TryAddWithoutValidation("Authorization", token); + + request.Content = note.ToNoteContent(); + using var client = new HttpClient(); + var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + // Deserialize to TriliumResponse + var triliumResponse = await response.Content.ReadFromJsonAsync(); + lastRequestTime = DateTimeOffset.UtcNow; + note.ObjectId = triliumResponse?.note.noteId; + note.Published = lastRequestTime; + note.TriliumNoteData = triliumResponse; + triliumNotes.Add(note); + } + + return triliumNotes; + } + + public Task> PatchNotesAsync(IEnumerable jiraIssues) + { + throw new NotImplementedException(); + // Update attributes + } + + public async Task FetchPageContentsAsync(Issue issue) { var baseUrl = _configs.AppSettings.Trilium.EtapiBaseUrl; var pageId = "QxxFqCNAtIOy"; @@ -34,4 +101,26 @@ public class TriliumService // Return new page ID after creating a new page return ""; } + + private string GetParentPageId(string issueStatus) + { + var readyPageId = _configs.AppSettings.Trilium.JiraOpenedIssuePageId; + var workingPageId = _configs.AppSettings.Trilium.JiraWorkingIssuePageId; + var resolvedPageId = _configs.AppSettings.Trilium.JiraResolvedIssuePageId; + string pageId = string.Empty; + if (issueStatus.Equals("Open", StringComparison.OrdinalIgnoreCase) + || issueStatus.Equals("Reopend", StringComparison.OrdinalIgnoreCase)) + { + pageId = readyPageId; + } + else if (issueStatus.Equals("In progress", StringComparison.OrdinalIgnoreCase)) + { + pageId = workingPageId; + } + else + { + pageId = resolvedPageId; + } + return pageId; + } } diff --git a/Workers/TriliumWorker.cs b/Workers/TriliumWorker.cs index e61c6a1..55e87d5 100644 --- a/Workers/TriliumWorker.cs +++ b/Workers/TriliumWorker.cs @@ -12,7 +12,7 @@ public class TriliumWorker : BackgroundService private readonly Serilog.ILogger _log; private readonly AppConfigs _config; private readonly IServiceScopeFactory _scopeFactory; // Singleton services cannot directly inject Scoped services. - private readonly TriliumService _triliumService; + private readonly TriliumService _trilium; private readonly Channel _issueChannel; public TriliumWorker(AppConfigs configs, IServiceScopeFactory serviceScopeFactory, @@ -21,7 +21,7 @@ public class TriliumWorker : BackgroundService _log = Log.ForContext(); _config = configs; _scopeFactory = serviceScopeFactory; - _triliumService = triliumService; + _trilium = triliumService; _issueChannel = issueChannel; } @@ -35,9 +35,7 @@ public class TriliumWorker : BackgroundService var db = scope.ServiceProvider.GetRequiredService(); var unpublishedIssues = await db.GetUnpublishedJiraIssuesAsync(stoppingToken); if (unpublishedIssues != null) - { - // Publish or update Trilium notes - } + await _trilium.PublishNotesAsync(unpublishedIssues); await Task.Delay(10 *1000, stoppingToken); }