162 lines
6.9 KiB
C#
162 lines
6.9 KiB
C#
using System.Net;
|
|||
|
|
|
||
|
|
using collAnon.Client.Services;
|
||
|
|
|
||
|
|
using decePubClient.Extensions;
|
||
|
|
using decePubClient.Models;
|
||
|
|
using decePubClient.Models.Mastodon;
|
||
|
|
|
||
|
|
using PrivaPub.ClientModels;
|
||
|
|
|
||
|
|
namespace decePubClient.Services
|
||
|
|
{
|
||
|
|
/// <summary>What the current persona does to and learns of other accounts.</summary>
|
||
|
|
public enum AccountAction
|
||
|
|
{
|
||
|
|
Follow,
|
||
|
|
Unfollow,
|
||
|
|
Block,
|
||
|
|
Unblock,
|
||
|
|
Mute,
|
||
|
|
Unmute
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Which of an account's posts its profile lists.</summary>
|
||
|
|
public enum ProfileTab
|
||
|
|
{
|
||
|
|
Posts,
|
||
|
|
PostsAndReplies,
|
||
|
|
Media
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Accounts as the current persona sees them: a profile, its relationship to the persona, its posts, following,
|
||
|
|
/// blocking and muting it, the persona's follow requests, and search across the fediverse (a remote handle or URL is
|
||
|
|
/// resolved by PrivaPub, never by the browser). Lists come back empty and values null on failure; nothing throws.
|
||
|
|
/// </summary>
|
||
|
|
public class AccountService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
||
|
|
{
|
||
|
|
const int PageSize = 20;
|
||
|
|
|
||
|
|
public async Task<Account> Get(string accountId, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_M_Account(accountId), cancellationToken: cancellationToken);
|
||
|
|
if (response.IsSuccessStatusCode)
|
||
|
|
return await response.MastodonReadFromJsonAsync<Account>(cancellationToken);
|
||
|
|
|
||
|
|
await LogFailure(response, nameof(Get), cancellationToken);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>An account by its handle (name or name@host), resolved by PrivaPub if it has not met it yet.</summary>
|
||
|
|
public async Task<Account> Lookup(string acct, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
acct = acct?.Trim().TrimStart('@');
|
||
|
|
if (string.IsNullOrEmpty(acct))
|
||
|
|
return null;
|
||
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_AccountLookup, [$"acct={Uri.EscapeDataString(acct)}"],
|
||
|
|
cancellationToken: cancellationToken);
|
||
|
|
if (response.IsSuccessStatusCode)
|
||
|
|
return await response.MastodonReadFromJsonAsync<Account>(cancellationToken);
|
||
|
|
|
||
|
|
var found = await Search(acct, resolve: true, cancellationToken);
|
||
|
|
return found.Accounts.FirstOrDefault(a => string.Equals(a.Acct, acct, StringComparison.OrdinalIgnoreCase))
|
||
|
|
?? found.Accounts.FirstOrDefault();
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<Relationship> Relationship(string accountId, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_Relationships, [$"id[]={Uri.EscapeDataString(accountId)}"],
|
||
|
|
cancellationToken: cancellationToken);
|
||
|
|
if (response.IsSuccessStatusCode)
|
||
|
|
return (await response.MastodonReadFromJsonAsync<List<Relationship>>(cancellationToken) ?? []).FirstOrDefault();
|
||
|
|
|
||
|
|
await LogFailure(response, nameof(Relationship), cancellationToken);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Follows, unfollows, blocks, unblocks, mutes or unmutes; Data is the new Relationship.</summary>
|
||
|
|
public async Task<WebResult> Act(string accountId, AccountAction action, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
using var response = await httpService.Post(APIs.Mastodon.POST_M_AccountAction(accountId, action.ToString().ToLowerInvariant()),
|
||
|
|
cancellationToken: cancellationToken);
|
||
|
|
if (!response.IsSuccessStatusCode)
|
||
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||
|
|
|
||
|
|
return new WebResult { Data = await response.MastodonReadFromJsonAsync<Relationship>(cancellationToken) };
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>A page of the account's posts, newest first; Data is a FeedPage.</summary>
|
||
|
|
public async Task<WebResult> Statuses(string accountId, ProfileTab tab, string maxId, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
var parameters = new List<string> { $"limit={PageSize}" };
|
||
|
|
parameters.AddRange(tab switch
|
||
|
|
{
|
||
|
|
ProfileTab.Posts => ["exclude_replies=true"],
|
||
|
|
ProfileTab.Media => ["only_media=true"],
|
||
|
|
_ => Array.Empty<string>()
|
||
|
|
});
|
||
|
|
if (maxId is not null)
|
||
|
|
parameters.Add($"max_id={Uri.EscapeDataString(maxId)}");
|
||
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_M_AccountStatuses(accountId), [.. parameters],
|
||
|
|
cancellationToken: cancellationToken);
|
||
|
|
if (!response.IsSuccessStatusCode)
|
||
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||
|
|
|
||
|
|
var statuses = await response.MastodonReadFromJsonAsync<List<Status>>(cancellationToken) ?? [];
|
||
|
|
var (nextMaxId, _) = response.PageCursors();
|
||
|
|
return new WebResult
|
||
|
|
{
|
||
|
|
Data = new FeedPage
|
||
|
|
{
|
||
|
|
Posts = [.. statuses.Select(FeedPost.Of)],
|
||
|
|
MaxId = statuses.Count < PageSize ? null : nextMaxId ?? statuses.LastOrDefault()?.Id
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>The accounts waiting for the persona to accept their follow.</summary>
|
||
|
|
public async Task<List<Account>> FollowRequests(CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_FollowRequests, ["limit=80"], cancellationToken: cancellationToken);
|
||
|
|
if (response.IsSuccessStatusCode)
|
||
|
|
return await response.MastodonReadFromJsonAsync<List<Account>>(cancellationToken) ?? [];
|
||
|
|
|
||
|
|
await LogFailure(response, nameof(FollowRequests), cancellationToken);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Accepts (authorize) or refuses (reject) a follow request.</summary>
|
||
|
|
public async Task<WebResult> Answer(string accountId, bool accept, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
using var response = await httpService.Post(APIs.Mastodon.POST_M_FollowRequest(accountId, accept ? "authorize" : "reject"),
|
||
|
|
cancellationToken: cancellationToken);
|
||
|
|
if (!response.IsSuccessStatusCode)
|
||
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||
|
|
|
||
|
|
return new WebResult { Data = await response.MastodonReadFromJsonAsync<Relationship>(cancellationToken) };
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Accounts, posts and hashtags matching the query; a handle or a URL is resolved when `resolve`.</summary>
|
||
|
|
public async Task<SearchResults> Search(string query, bool resolve, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(query))
|
||
|
|
return new SearchResults();
|
||
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_Search,
|
||
|
|
[$"q={Uri.EscapeDataString(query.Trim())}", $"resolve={(resolve ? "true" : "false")}", "limit=20"],
|
||
|
|
cancellationToken: cancellationToken);
|
||
|
|
if (response.IsSuccessStatusCode)
|
||
|
|
return await response.MastodonReadFromJsonAsync<SearchResults>(cancellationToken) ?? new SearchResults();
|
||
|
|
|
||
|
|
await LogFailure(response, nameof(Search), cancellationToken);
|
||
|
|
return new SearchResults();
|
||
|
|
}
|
||
|
|
|
||
|
|
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(AccountService)}.{caller}", cancellationToken);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|