using System.Net; using collAnon.Client.Services; using decePubClient.Extensions; using decePubClient.Models; using decePubClient.Models.Mastodon; using PrivaPub.ClientModels; namespace decePubClient.Services { /// /// The current persona's lists (Mastodon's /api/v1/lists): made, renamed and deleted, their members (only accounts the /// persona follows; PrivaPub drops a member when the follow ends), and the lists an account is in. A list's posts are a /// feed of their own (TimelineService, TimelineType.Lists). Lists come back empty on failure; nothing throws. /// public class ListService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer) { /// The most members PrivaPub keeps in one list, and so the most read back. public const int MaxMembers = 500; const int MembersPerPage = 80; public async Task> All(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.Mastodon.GET_Lists, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.MastodonReadFromJsonAsync>(cancellationToken) ?? []; await LogFailure(response, nameof(All), cancellationToken); return []; } /// Makes the list when it has no id yet, or saves its title, replies policy and exclusivity; Data is the list. public async Task Save(MastodonList list, CancellationToken cancellationToken = default) { var form = new MastodonList { Title = list.Title?.Trim(), RepliesPolicy = list.RepliesPolicy, Exclusive = list.Exclusive }; using var response = list.Id is null ? await httpService.Post(APIs.Mastodon.POST_List, form, cancellationToken: cancellationToken) : await httpService.Put(APIs.Mastodon.PUT_M_List(list.Id), form, cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); return new WebResult { Data = await response.MastodonReadFromJsonAsync(cancellationToken) }; } public async Task Delete(string listId, CancellationToken cancellationToken = default) { using var response = await httpService.Delete(APIs.Mastodon.DELETE_M_List(listId), cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); return new WebResult(); } /// Every member of the list, page by page, up to MaxMembers. public async Task> Members(string listId, CancellationToken cancellationToken = default) { var members = new List(); var maxId = default(string); while (members.Count < MaxMembers) { string[] parameters = maxId is null ? [$"limit={MembersPerPage}"] : [$"limit={MembersPerPage}", $"max_id={Uri.EscapeDataString(maxId)}"]; using var response = await httpService.Get(APIs.Mastodon.M_ListAccounts(listId), parameters, cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) { await LogFailure(response, nameof(Members), cancellationToken); return members; } var page = await response.MastodonReadFromJsonAsync>(cancellationToken) ?? []; members.AddRange(page); maxId = response.PageCursors().MaxId; if (page.Count < MembersPerPage || maxId is null) return members; } return members; } /// Adds (or removes) an account the persona follows to (from) the list. public async Task SetMember(string listId, string accountId, bool member, CancellationToken cancellationToken = default) { using var response = member ? await httpService.Post(APIs.Mastodon.M_ListAccounts(listId), new ListAccountsForm { AccountIds = [accountId] }, cancellationToken: cancellationToken) : await httpService.Delete(APIs.Mastodon.M_ListAccounts(listId), [$"account_ids[]={Uri.EscapeDataString(accountId)}"], cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); return new WebResult(); } /// The persona's lists the account is in. public async Task> Of(string accountId, CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.Mastodon.GET_M_AccountLists(accountId), cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.MastodonReadFromJsonAsync>(cancellationToken) ?? []; await LogFailure(response, nameof(Of), cancellationToken); return []; } /// Accounts the persona follows whose name, display name or server starts with the query (PrivaPub's /// following=true; nothing is resolved). public async Task> FindFollowed(string query, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(query)) return []; using var response = await httpService.Get(APIs.Mastodon.GET_AccountSearch, [$"q={Uri.EscapeDataString(query.Trim())}", "following=true", "resolve=false", "limit=20"], cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.MastodonReadFromJsonAsync>(cancellationToken) ?? []; await LogFailure(response, nameof(FindFollowed), cancellationToken); return []; } async Task LogFailure(HttpResponseMessage response, string caller, CancellationToken cancellationToken) { var result = await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); if (result.StatusCode is not ((int)HttpStatusCode.Gone or (int)HttpStatusCode.NotFound)) await logger.ProcessWarning(result.ToString(localizer), $"{nameof(ListService)}.{caller}", cancellationToken); } } }