namespace OF_DL.Services; public class CajetanApiService(IAuthService authService, IConfigService configService, ICajetanDbService dbService, ICajetanDownloadEventHandler eventHandler) : ApiService(authService, configService, dbService), ICajetanApiService { private readonly ICajetanDownloadEventHandler _eventHandler = eventHandler; public new async Task GetUserInfo(string endpoint) { UserEntities.UserInfo? userInfo = await GetDetailedUserInfoAsync(endpoint); if (userInfo is not null && !endpoint.EndsWith("/me")) await dbService.UpdateUserInfoAsync(userInfo); return userInfo; } public new Task?> GetActiveSubscriptions(string endpoint, bool includeRestricted) { Log.Debug("Calling GetActiveSubscriptions"); return GetAllSubscriptions(endpoint, includeRestricted, "active"); } public new Task?> GetExpiredSubscriptions(string endpoint, bool includeRestricted) { Log.Debug("Calling GetExpiredSubscriptions"); return GetAllSubscriptions(endpoint, includeRestricted, "expired"); } public new async Task GetMessages(string endpoint, string folder, IStatusReporter statusReporter) { (bool couldExtract, long userId) = ExtractUserId(endpoint); _eventHandler.OnMessage("Getting Unread Chats", "grey"); HashSet 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 new async Task?> GetListUsers(string endpoint) { if (!HasSignedRequestAuth()) return null; try { Dictionary users = new(StringComparer.OrdinalIgnoreCase); Log.Debug($"Calling GetListUsers - {endpoint}"); const int limit = 50; int offset = 0; Dictionary getParams = new() { ["format"] = "infinite", ["limit"] = limit.ToString() }; while (true) { getParams["offset"] = offset.ToString(); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (string.IsNullOrWhiteSpace(body)) break; ListsDtos.ListUsersDto? listUsers = DeserializeJson(body, s_mJsonSerializerSettings); if (listUsers?.List is null) break; foreach (ListsDtos.UsersListDto item in listUsers.List) { if (item.Id is null) continue; users.TryAdd(item.Username, item.Id.Value); } if (listUsers.HasMore is false) break; offset = listUsers.NextOffset; } return users; } catch (Exception ex) { ExceptionLoggerHelper.LogException(ex); } return null; } public async Task GetDetailedUserInfoAsync(string endpoint) { Log.Debug($"Calling GetDetailedUserInfo: {endpoint}"); if (!HasSignedRequestAuth()) return null; try { UserEntities.UserInfo userInfo = new(); Dictionary getParams = new() { { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_asc" } }; HttpClient client = new(); HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); using HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) return userInfo; response.EnsureSuccessStatusCode(); string body = await response.Content.ReadAsStringAsync(); UserDtos.UserDto? userDto = JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); userInfo = FromDto(userDto); return userInfo; } catch (Exception ex) { ExceptionLoggerHelper.LogException(ex); } return null; } public async Task> GetUsersWithProgressAsync(string typeDisplay, string endpoint, string? typeParam, bool offsetByCount) { Dictionary usersOfType = await _eventHandler.WithStatusAsync( statusMessage: $"Getting {typeDisplay} Users", work: FetchAsync ); return usersOfType; async Task> FetchAsync(IStatusReporter statusReporter) { Dictionary users = []; int limit = 50; int offset = 0; bool includeRestricted = true; Dictionary getParams = new() { ["format"] = "infinite", ["limit"] = limit.ToString(), ["offset"] = offset.ToString() }; if (!string.IsNullOrWhiteSpace(typeParam)) getParams["type"] = typeParam; try { Log.Debug("Calling GetUsersWithProgress"); HttpClient client = GetHttpClient(); bool isLastLoop = false; while (true) { string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, client); if (string.IsNullOrWhiteSpace(body)) break; SubscriptionsDtos.SubscriptionsDto? subscriptions = DeserializeJson(body, s_mJsonSerializerSettings); if (subscriptions?.List is null) break; foreach (SubscriptionsDtos.ListItemDto item in subscriptions.List) { if (string.IsNullOrWhiteSpace(item?.Username)) continue; if (users.ContainsKey(item.Username)) continue; bool isRestricted = item.IsRestricted ?? false; bool isRestrictedButAllowed = isRestricted && includeRestricted; if (!isRestricted || isRestrictedButAllowed) users.Add(item.Username, item.Id); } statusReporter.ReportStatus($"[blue]Getting {typeDisplay} Users\n[/] [blue]Found {users.Count}[/]"); if (isLastLoop) break; if (!subscriptions.HasMore || subscriptions.List.Count == 0) isLastLoop = true; offset += offsetByCount ? subscriptions.List.Count : limit; getParams["offset"] = offset.ToString(); } } catch (Exception ex) { ExceptionLoggerHelper.LogException(ex); } return users; } } public async Task> GetUsersWithUnreadMessagesAsync() { MessageDtos.ChatsDto unreadChats = await GetChatsAsync("/chats", onlyUnread: true); HashSet 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); } } public async Task SortBlockedAsync(string endpoint, string order = "recent", string direction = "desc") { Log.Debug($"Calling SortBlocked - {endpoint}"); try { var reqBody = new { order, direction }; var result = new { success = false, canAddFriends = false }; string? body = await BuildHeaderAndExecuteRequests([], endpoint, GetHttpClient(), HttpMethod.Post, reqBody); if (!string.IsNullOrWhiteSpace(body)) result = JsonConvert.DeserializeAnonymousType(body, result); if (result?.success != true) _eventHandler.OnMessage($"Failed to sort blocked (order: {order}, direction; {direction})! Endpoint: {endpoint}", "yellow"); } catch (Exception ex) { ExceptionLoggerHelper.LogException(ex); } } private async Task?> GetAllSubscriptions(string endpoint, bool includeRestricted, string type) { if (!HasSignedRequestAuth()) return null; try { Dictionary users = new(StringComparer.OrdinalIgnoreCase); Log.Debug("Calling GetAllSubscrptions"); const int limit = 50; int offset = 0; Dictionary getParams = new() { ["type"] = type, ["format"] = "infinite", ["limit"] = limit.ToString(), }; while (true) { getParams["offset"] = offset.ToString(); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (string.IsNullOrWhiteSpace(body)) break; SubscriptionsDtos.SubscriptionsDto? subscriptionsDto = DeserializeJson(body, s_mJsonSerializerSettings); if (subscriptionsDto?.List is null) break; foreach (SubscriptionsDtos.ListItemDto item in subscriptionsDto.List) { if (string.IsNullOrWhiteSpace(item.Username)) { Log.Warning("Found '{Type:l}' subscription user with empty username (id: {Id})", type, item.Id); continue; } if (users.ContainsKey(item.Username)) continue; bool isRestricted = item.IsRestricted ?? false; bool isRestrictedButAllowed = isRestricted && includeRestricted; if (!isRestricted || isRestrictedButAllowed) users.Add(item.Username, item.Id); } if (subscriptionsDto.HasMore is false) break; offset += limit; } return users; } catch (Exception ex) { ExceptionLoggerHelper.LogException(ex); } return null; } private async Task GetChatsAsync(string endpoint, bool onlyUnread) { Log.Debug($"Calling GetChats - {endpoint}"); MessageDtos.ChatsDto allChats = new(); try { const int limit = 60; int offset = 0; Dictionary getParams = new() { ["order"] = "recent", ["skip_users"] = "all", ["filter"] = "unread", ["limit"] = $"{limit}", }; if (onlyUnread is false) getParams.Remove("filter"); while (true) { getParams["offset"] = $"{offset}"; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); if (string.IsNullOrWhiteSpace(body)) break; MessageDtos.ChatsDto? chats = DeserializeJson(body, s_mJsonSerializerSettings); if (chats?.List is null) break; allChats.List.AddRange(chats.List); if (chats.HasMore is false) break; offset = chats.NextOffset; } } catch (Exception ex) { ExceptionLoggerHelper.LogException(ex); } return allChats; } private static UserEntities.UserInfo FromDto(UserDtos.UserDto? userDto) { if (userDto is null) return new(); return new() { Id = userDto.Id, Avatar = userDto.Avatar, Header = userDto.Header, Name = userDto.Name, Username = userDto.Username, SubscribePrice = userDto.SubscribePrice, CurrentSubscribePrice = userDto.CurrentSubscribePrice, IsPaywallRequired = userDto.IsPaywallRequired, IsRestricted = userDto.IsRestricted, SubscribedBy = userDto.SubscribedBy, SubscribedByExpire = userDto.SubscribedByExpire, SubscribedByExpireDate = userDto.SubscribedByExpireDate, SubscribedByAutoprolong = userDto.SubscribedByAutoprolong, SubscribedIsExpiredNow = userDto.SubscribedIsExpiredNow, SubscribedOn = userDto.SubscribedOn, SubscribedOnExpiredNow = userDto.SubscribedOnExpiredNow, SubscribedOnDuration = userDto.SubscribedOnDuration, About = userDto.About, PostsCount = userDto.PostsCount, ArchivedPostsCount = userDto.ArchivedPostsCount, PrivateArchivedPostsCount = userDto.PrivateArchivedPostsCount, PhotosCount = userDto.PhotosCount, VideosCount = userDto.VideosCount, AudiosCount = userDto.AudiosCount, MediasCount = userDto.MediasCount, }; } }