Compare commits
7 Commits
cf0b6907f7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f6bccc5434 | |||
| 2e2edc8dac | |||
| 5ec2f8119e | |||
| 4cb7797314 | |||
| 9b66273d20 | |||
| b904fa8b2d | |||
| 92a5195183 |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
29
Models/TriliumNote.cs
Normal file
29
Models/TriliumNote.cs
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
48
Models/TriliumResponse.cs
Normal file
48
Models/TriliumResponse.cs
Normal file
@@ -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; }
|
||||
}
|
||||
@@ -203,5 +203,23 @@ public class AppDbService
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<JiraIssue>> GetUnpublishedJiraIssuesAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unpublishedIssues = await _db.JiraIssues
|
||||
.Where(ji => ji.Published < ji.Updated)
|
||||
.ToListAsync(ct);
|
||||
|
||||
_log.Information("Found {count} unpublished Jira issues", unpublishedIssues.Count);
|
||||
return unpublishedIssues;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex, "Failed to get unpublished Jira issues");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion Table: JiraIssues
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class JiraService
|
||||
+ $"AND updated >= \"{lastFetchTime.ToString("yyyy-MM-dd HH:mm")}\" "
|
||||
+ $"&fields={_configs.AppSettings.Jira.SearchFields}"
|
||||
+ $"&startAt={startAt}&maxResults=50";
|
||||
Log.Verbose("Jira API Request URI: {uri}", uri);
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
request.Headers.Add("Accept", "application/json");
|
||||
request.Headers.Add("Authorization", $"Bearer {_configs.AppSettings.Jira.AccessToken}");
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using TriliumMind.Data.Entities;
|
||||
using System;
|
||||
using System.Reflection.Metadata;
|
||||
using TriliumMind.Data.Entities;
|
||||
using TriliumMind.Models;
|
||||
|
||||
namespace TriliumMind.Services.Mappers;
|
||||
|
||||
public static class JiraIssueMapper
|
||||
{
|
||||
private static readonly char[] _specialCharacters = ['(', '/'];
|
||||
|
||||
public static JiraIssue ToEntity(this Issue issue)
|
||||
{
|
||||
return new JiraIssue
|
||||
@@ -13,9 +17,9 @@ public static class JiraIssueMapper
|
||||
Summary = issue.fields.summary ?? string.Empty,
|
||||
Parent = issue.fields.parent?.key ?? issue.fields.customfield_10808,
|
||||
Type = issue.fields.issuetype?.name ?? string.Empty,
|
||||
Status = issue.fields.status?.description ?? string.Empty,
|
||||
Assignee = issue.fields.assignee?.displayName ?? string.Empty,
|
||||
Manager = issue.fields.reporter?.displayName ?? string.Empty,
|
||||
Status = issue.fields.status?.name ?? string.Empty,
|
||||
Assignee = CleanDisplayName(issue.fields.assignee?.displayName),
|
||||
Manager = CleanDisplayName(issue.fields.reporter?.displayName),
|
||||
Due = issue.fields.duedate?.ToUniversalTime() ?? DateTimeOffset.MinValue.ToUniversalTime(),
|
||||
Updated = issue.fields.UpdatedAt.ToUniversalTime(),
|
||||
Published = DateTimeOffset.MinValue.ToUniversalTime(),
|
||||
@@ -23,4 +27,15 @@ public static class JiraIssueMapper
|
||||
NeedNotify = 0
|
||||
};
|
||||
}
|
||||
private static string CleanDisplayName(string? displayName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
return string.Empty;
|
||||
|
||||
var specialCharIndex = displayName.IndexOfAny(_specialCharacters);
|
||||
if (specialCharIndex >= 0)
|
||||
displayName = displayName[..specialCharIndex];
|
||||
|
||||
return displayName.Trim();
|
||||
}
|
||||
}
|
||||
@@ -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<TriliumService>();
|
||||
_configs = configs;
|
||||
}
|
||||
public async Task<string> FettchPageContentsAsync(Issue issue)
|
||||
|
||||
public async Task<IEnumerable<JiraIssue>> PublishNotesAsync(IEnumerable<JiraIssue> jiraIssues)
|
||||
{
|
||||
var issuesToPatch = new List<JiraIssue>();
|
||||
var issuesToPost = new List<JiraIssue>();
|
||||
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<IEnumerable<TriliumNote>> PostNotesAsync(IEnumerable<JiraIssue> jiraIssues)
|
||||
{
|
||||
var baseUrl = _configs.AppSettings.Trilium.EtapiBaseUrl;
|
||||
var token = _configs.AppSettings.Trilium.EtapiToken;
|
||||
var apiUrl = $"{baseUrl}/create-note";
|
||||
|
||||
var triliumNotes = new List<TriliumNote>();
|
||||
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<TriliumResponse>();
|
||||
lastRequestTime = DateTimeOffset.UtcNow;
|
||||
note.ObjectId = triliumResponse?.note.noteId;
|
||||
note.Published = lastRequestTime;
|
||||
note.TriliumNoteData = triliumResponse;
|
||||
triliumNotes.Add(note);
|
||||
}
|
||||
|
||||
return triliumNotes;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<JiraIssue>> PatchNotesAsync(IEnumerable<JiraIssue> jiraIssues)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// Update attributes
|
||||
}
|
||||
|
||||
public async Task<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +108,6 @@ public class AppDbWorker : BackgroundService
|
||||
{
|
||||
_log.Error(ex, "Fatal error in JiraDbWorker");
|
||||
}
|
||||
|
||||
//await foreach (var issue in _issueChannel.Reader.ReadAllAsync(stoppingToken))
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// var jiraIssue = issue.ToEntity();
|
||||
// await _db.UpsertJiraIssueAsync(jiraIssue, stoppingToken);
|
||||
// _log.Debug("Processed Jira issue: {key}", issue.key);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _log.Error(ex, "Failed to process Jira issue: {key}", issue.key);
|
||||
// }
|
||||
//}
|
||||
_log.Information("AppDbWorker stopped");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,17 @@ public class TriliumWorker : BackgroundService
|
||||
{
|
||||
private readonly Serilog.ILogger _log;
|
||||
private readonly AppConfigs _config;
|
||||
private readonly TriliumService _triliumService;
|
||||
private readonly IServiceScopeFactory _scopeFactory; // Singleton services cannot directly inject Scoped services.
|
||||
private readonly TriliumService _trilium;
|
||||
private readonly Channel<Issue> _issueChannel;
|
||||
|
||||
public TriliumWorker(AppConfigs configs, TriliumService triliumService, Channel<Issue> issueChannel)
|
||||
public TriliumWorker(AppConfigs configs, IServiceScopeFactory serviceScopeFactory,
|
||||
TriliumService triliumService, Channel<Issue> issueChannel)
|
||||
{
|
||||
_log = Log.ForContext<TriliumWorker>();
|
||||
_config = configs;
|
||||
_triliumService = triliumService;
|
||||
_scopeFactory = serviceScopeFactory;
|
||||
_trilium = triliumService;
|
||||
_issueChannel = issueChannel;
|
||||
}
|
||||
|
||||
@@ -27,6 +30,13 @@ public class TriliumWorker : BackgroundService
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_log.Debug("Worker running at: {time}", DateTimeOffset.Now);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbService>();
|
||||
var unpublishedIssues = await db.GetUnpublishedJiraIssuesAsync(stoppingToken);
|
||||
if (unpublishedIssues != null)
|
||||
await _trilium.PublishNotesAsync(unpublishedIssues);
|
||||
|
||||
await Task.Delay(10 *1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user