Implemented unread chats handling when fetching messages

This commit is contained in:
Casper Sparre 2026-02-19 20:59:02 +01:00
parent 161faac8a5
commit ea3346c454
9 changed files with 202 additions and 17 deletions

View File

@ -2,7 +2,12 @@ using OF_DL.Models.Downloads;
namespace OF_DL.CLI; namespace OF_DL.CLI;
internal class CajetanDownloadEventHandler : IDownloadEventHandler public interface ICajetanDownloadEventHandler : IDownloadEventHandler
{
void OnMessage(string message, string color);
}
public class CajetanDownloadEventHandler : ICajetanDownloadEventHandler
{ {
private readonly SpectreDownloadEventHandler _eventHandler = new(); private readonly SpectreDownloadEventHandler _eventHandler = new();
@ -15,6 +20,9 @@ internal class CajetanDownloadEventHandler : IDownloadEventHandler
public void OnMessage(string message) public void OnMessage(string message)
=> _eventHandler.OnMessage(message); => _eventHandler.OnMessage(message);
public void OnMessage(string message, string color)
=> AnsiConsole.Markup($"[{color.ToLowerInvariant()}]{Markup.Escape(message)}\n[/]");
public void OnNoContentFound(string contentType) public void OnNoContentFound(string contentType)
=> _eventHandler.OnNoContentFound(contentType); => _eventHandler.OnNoContentFound(contentType);

View File

@ -0,0 +1,19 @@
namespace OF_DL.Models.Dtos.Messages;
public class ChatsDto
{
[JsonProperty("list")] public List<ChatItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool HasMore { get; set; }
[JsonProperty("nextOffset")] public int NextOffset { get; set; }
}
public class ChatItemDto
{
[JsonProperty("withUser")] public ChatUserDto WithUser { get; set; } = new();
[JsonProperty("unreadMessagesCount")] public int UnreadMessagesCount { get; set; }
}
public class ChatUserDto
{
[JsonProperty("id")] public long Id { get; set; }
}

View File

@ -3,14 +3,18 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red)); AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red));
await RunAsync(args);
ServiceCollection services = await ConfigureServices(args); static async Task RunAsync(string[] args)
ServiceProvider serviceProvider = services.BuildServiceProvider(); {
ServiceCollection services = await ConfigureServices(args);
ServiceProvider serviceProvider = services.BuildServiceProvider();
ExitIfOtherProcess(serviceProvider); ExitIfOtherProcess(serviceProvider);
Worker worker = serviceProvider.GetRequiredService<Worker>(); Worker worker = serviceProvider.GetRequiredService<Worker>();
await worker.RunAsync(); await worker.RunAsync();
}
static async Task<ServiceCollection> ConfigureServices(string[] args) static async Task<ServiceCollection> ConfigureServices(string[] args)
{ {
@ -62,6 +66,11 @@ static async Task<ServiceCollection> ConfigureServices(string[] args)
services.AddSingleton(cajetanConfig); services.AddSingleton(cajetanConfig);
services.AddSingleton<IAuthService, AuthService>(); services.AddSingleton<IAuthService, AuthService>();
services.AddSingleton<IStartupService, StartupService>();
services.AddSingleton<IFileNameService, FileNameService>();
services.AddSingleton<ICajetanDownloadService, CajetanDownloadService>();
services.AddSingleton<IDownloadService>(sp => sp.GetRequiredService<ICajetanDownloadService>());
services.AddSingleton<ICajetanApiService, CajetanApiService>(); services.AddSingleton<ICajetanApiService, CajetanApiService>();
services.AddSingleton<IApiService>(sp => sp.GetRequiredService<ICajetanApiService>()); services.AddSingleton<IApiService>(sp => sp.GetRequiredService<ICajetanApiService>());
@ -69,10 +78,12 @@ static async Task<ServiceCollection> ConfigureServices(string[] args)
services.AddSingleton<ICajetanDbService, CajetanDbService>(); services.AddSingleton<ICajetanDbService, CajetanDbService>();
services.AddSingleton<IDbService>(sp => sp.GetRequiredService<ICajetanDbService>()); services.AddSingleton<IDbService>(sp => sp.GetRequiredService<ICajetanDbService>());
services.AddSingleton<IDownloadService, DownloadService>();
services.AddSingleton<IFileNameService, FileNameService>(); services.AddSingleton<ICajetanDownloadOrchestrationService, CajetanDownloadOrchestrationService>();
services.AddSingleton<IStartupService, StartupService>(); services.AddSingleton<IDownloadOrchestrationService>(sp => sp.GetRequiredService<ICajetanDownloadOrchestrationService>());
services.AddSingleton<IDownloadOrchestrationService, DownloadOrchestrationService>();
services.AddSingleton<ICajetanDownloadEventHandler, CajetanDownloadEventHandler>();
services.AddSingleton<IDownloadEventHandler>(sp => sp.GetRequiredService<ICajetanDownloadEventHandler>());
services.AddSingleton<Worker>(); services.AddSingleton<Worker>();

View File

@ -1,7 +1,6 @@
global using Newtonsoft.Json; global using Newtonsoft.Json;
global using OF_DL; global using OF_DL;
global using OF_DL.CLI; global using OF_DL.CLI;
global using OF_DL.Crypto;
global using OF_DL.Enumerations; global using OF_DL.Enumerations;
global using OF_DL.Exceptions; global using OF_DL.Exceptions;
global using OF_DL.Helpers; global using OF_DL.Helpers;

View File

@ -1,13 +1,10 @@
namespace OF_DL.Services; namespace OF_DL.Services;
public interface ICajetanApiService : IApiService public class CajetanApiService(IAuthService authService, IConfigService configService, ICajetanDbService dbService, ICajetanDownloadEventHandler eventHandler)
{
Task<UserEntities.UserInfo?> GetDetailedUserInfo(string endpoint);
}
public class CajetanApiService(IAuthService authService, IConfigService configService, ICajetanDbService dbService)
: ApiService(authService, configService, dbService), ICajetanApiService : ApiService(authService, configService, dbService), ICajetanApiService
{ {
private readonly ICajetanDownloadEventHandler _eventHandler = eventHandler;
public new async Task<UserEntities.User?> GetUserInfo(string endpoint) public new async Task<UserEntities.User?> GetUserInfo(string endpoint)
{ {
UserEntities.UserInfo? userInfo = await GetDetailedUserInfoAsync(endpoint); UserEntities.UserInfo? userInfo = await GetDetailedUserInfoAsync(endpoint);
@ -55,6 +52,133 @@ public class CajetanApiService(IAuthService authService, IConfigService configSe
return null; return null;
} }
public new async Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, IStatusReporter statusReporter)
{
(bool couldExtract, long userId) = ExtractUserId(endpoint);
_eventHandler.OnMessage("Getting Unread Chats", "grey");
HashSet<long> usersWithUnread = couldExtract ? await GetUsersWithUnreadMessagesAsync() : [];
MessageEntities.MessageCollection messages = await base.GetMessages(endpoint, folder, statusReporter);
if (usersWithUnread.Contains(userId))
{
_eventHandler.OnMessage("Restoring unread state", "grey");
await MarkAsUnreadAsync($"/chats/{userId}/mark-as-read");
}
return messages;
static (bool couldExtract, long userId) ExtractUserId(string endpoint)
{
string withoutChatsAndMessages = endpoint
.Replace("chats", "", StringComparison.OrdinalIgnoreCase)
.Replace("messages", "", StringComparison.OrdinalIgnoreCase);
string trimmed = withoutChatsAndMessages.Trim(' ', '/', '\\');
if (long.TryParse(trimmed, out long userId))
return (true, userId);
return (false, default);
}
}
public async Task<HashSet<long>> GetUsersWithUnreadMessagesAsync()
{
MessageDtos.ChatsDto unreadChats = await GetChatsAsync("/chats", onlyUnread: true);
HashSet<long> userWithUnread = [];
foreach (MessageDtos.ChatItemDto chatItem in unreadChats.List)
{
if (chatItem?.WithUser?.Id is null)
continue;
if (chatItem.UnreadMessagesCount <= 0)
continue;
userWithUnread.Add(chatItem.WithUser.Id);
}
return userWithUnread;
}
public async Task MarkAsUnreadAsync(string endpoint)
{
Log.Debug($"Calling MarkAsUnread - {endpoint}");
try
{
var result = new { success = false };
string? body = await BuildHeaderAndExecuteRequests([], endpoint, GetHttpClient(), HttpMethod.Delete);
if (!string.IsNullOrWhiteSpace(body))
result = JsonConvert.DeserializeAnonymousType(body, result);
if (result?.success != true)
_eventHandler.OnMessage($"Failed to mark chat as unread! Endpoint: {endpoint}", "yellow");
}
catch (Exception ex)
{
ExceptionLoggerHelper.LogException(ex);
}
}
private async Task<MessageDtos.ChatsDto> GetChatsAsync(string endpoint, bool onlyUnread)
{
Log.Debug($"Calling GetChats - {endpoint}");
MessageDtos.ChatsDto allChats = new();
try
{
int limit = 60;
Dictionary<string, string> getParams = new()
{
{ "limit", $"{limit}" },
{ "offset", "0" },
{ "skip_users", "all" },
{ "order", "recent" }
};
if (onlyUnread)
getParams["filter"] = "unread";
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
MessageDtos.ChatsDto? chats = DeserializeJson<MessageDtos.ChatsDto>(body, s_mJsonSerializerSettings);
if (chats is null)
return allChats;
if (chats.HasMore)
{
getParams["offset"] = $"{chats.NextOffset}";
while (true)
{
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
MessageDtos.ChatsDto? newChats = DeserializeJson<MessageDtos.ChatsDto>(loopbody, s_mJsonSerializerSettings);
if (newChats is null)
break;
allChats.List.AddRange(newChats.List);
if (!newChats.HasMore)
break;
getParams["offset"] = $"{newChats.NextOffset}";
}
}
}
catch (Exception ex)
{
ExceptionLoggerHelper.LogException(ex);
}
return allChats;
}
private static UserEntities.UserInfo FromDto(UserDtos.UserDto? userDto) private static UserEntities.UserInfo FromDto(UserDtos.UserDto? userDto)
{ {
if (userDto is null) if (userDto is null)

View File

@ -0,0 +1,6 @@
namespace OF_DL.Services;
public class CajetanDownloadOrchestrationService(ICajetanApiService apiService, IConfigService configService, ICajetanDownloadService downloadService, ICajetanDbService dbService)
: DownloadOrchestrationService(apiService, configService, downloadService, dbService), ICajetanDownloadOrchestrationService
{
}

View File

@ -0,0 +1,6 @@
namespace OF_DL.Services;
public class CajetanDownloadService(IAuthService authService, IConfigService configService, ICajetanDbService dbService, IFileNameService fileNameService, ICajetanApiService apiService)
: DownloadService(authService, configService, dbService, fileNameService, apiService), ICajetanDownloadService
{
}

View File

@ -0,0 +1,6 @@
namespace OF_DL.Services;
public interface ICajetanDownloadOrchestrationService : IDownloadOrchestrationService
{
}

View File

@ -0,0 +1,6 @@
namespace OF_DL.Services;
public interface ICajetanDownloadService : IDownloadService
{
}