The composer's clock button gives a post a time (scheduled_at, at least five minutes ahead; located and group posts have none), and /scheduled moves or drops what waits. A hashtag's feed tab shows its week and follows it, so its public posts come home. Explore (/explore) shows what PrivaPub says is trending, hashtags, posts and links from public posts it holds, the directory by activity or by joining, and the hashtags the persona follows. DiscoveryService does the calls; the e2e tests check each against PrivaPub's own answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
86 lines
3.9 KiB
C#
86 lines
3.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>Which order the directory lists accounts in.</summary>
|
|
public enum DirectoryOrder
|
|
{
|
|
/// <summary>By their latest public post.</summary>
|
|
Active,
|
|
/// <summary>By the day they say they joined.</summary>
|
|
New
|
|
}
|
|
|
|
/// <summary>
|
|
/// What there is to find on PrivaPub: hashtags (their week, following them), what is trending there (hashtags, posts
|
|
/// and links, from public posts it already holds) and the directory of accounts that let themselves be discovered.
|
|
/// Lists come back empty and values null on failure; nothing throws.
|
|
/// </summary>
|
|
public class DiscoveryService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
|
{
|
|
public async Task<Tag> Tag(string name, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.GetAnon(APIs.Mastodon.GET_M_Tag(name), cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.MastodonReadFromJsonAsync<Tag>(cancellationToken);
|
|
|
|
await LogFailure(response, nameof(Tag), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Follows or stops following a hashtag; Data is the Tag.</summary>
|
|
public async Task<WebResult> FollowTag(string name, bool follow, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.Mastodon.POST_M_TagAction(name, follow ? "follow" : "unfollow"), cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.MastodonReadFromJsonAsync<Tag>(cancellationToken) };
|
|
}
|
|
|
|
public Task<List<Tag>> FollowedTags(CancellationToken cancellationToken = default) =>
|
|
List<Tag>(APIs.Mastodon.GET_FollowedTags, ["limit=200"], nameof(FollowedTags), cancellationToken);
|
|
|
|
public Task<List<Tag>> TrendingTags(CancellationToken cancellationToken = default) =>
|
|
List<Tag>(APIs.Mastodon.GET_TrendingTags, ["limit=20"], nameof(TrendingTags), cancellationToken);
|
|
|
|
public Task<List<Status>> TrendingStatuses(CancellationToken cancellationToken = default) =>
|
|
List<Status>(APIs.Mastodon.GET_TrendingStatuses, ["limit=40"], nameof(TrendingStatuses), cancellationToken);
|
|
|
|
public Task<List<TrendLink>> TrendingLinks(CancellationToken cancellationToken = default) =>
|
|
List<TrendLink>(APIs.Mastodon.GET_TrendingLinks, ["limit=20"], nameof(TrendingLinks), cancellationToken);
|
|
|
|
public const int DirectoryPage = 40;
|
|
|
|
public Task<List<Account>> Directory(DirectoryOrder order, bool localOnly, int offset, CancellationToken cancellationToken = default) =>
|
|
List<Account>(APIs.Mastodon.GET_Directory,
|
|
[$"order={(order is DirectoryOrder.New ? "new" : "active")}", $"local={(localOnly ? "true" : "false")}", $"limit={DirectoryPage}", $"offset={offset}"],
|
|
nameof(Directory), cancellationToken);
|
|
|
|
async Task<List<T>> List<T>(string route, string[] parameters, string caller, CancellationToken cancellationToken)
|
|
{
|
|
using var response = await httpService.GetAnon(route, parameters, cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.MastodonReadFromJsonAsync<List<T>>(cancellationToken) ?? [];
|
|
|
|
await LogFailure(response, caller, 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(DiscoveryService)}.{caller}", cancellationToken);
|
|
}
|
|
}
|
|
}
|