R8 the Mastodon API made real: featured tags, endorsements, notes, notify and languages, suggestions, the direct stream
Featured tags per persona (/api/v1/featured_tags, suggestions, tags/:name/feature, accounts' featured tags, max 10),
listed at /tattoos as Hashtags and sent as Add/Remove{Hashtag} to followers on the featured collection, as Mastodon
sends them; remote accounts' tags received and read from featuredTags. Endorsements (pin/endorse) and private notes
(PersonaNote) kept per persona and never federated; the relationship's notifying, languages, endorsed and note, with
follow's notify (status notifications) and languages (home filter). Reading preferences set on /clientapi. Suggestions
v1/v2 with dismissals and staff picks, never the persona or a sibling. conversation events on the direct stream.
translate, annual reports and the admin API answer. New collections backed up and removed with their persona.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
d08fbabaa5
commit
87529bca49
42 files changed
+1505
-77
No files matched your search
@@ -26,7 +26,10 @@ namespace PrivaPub.Domain.Social
|
||||
Task<WebResult> Follow(string rootUserId, FollowForm form, CancellationToken token);
|
||||
Task<WebResult> Unfollow(string rootUserId, FollowForm form, CancellationToken token);
|
||||
Task<WebResult> Followings(string rootUserId, string avatarId, CancellationToken token);
|
||||
Task<Following> FollowAs(LocalActor follower, string target, bool showReblogs, CancellationToken token);
|
||||
/// <summary>Follows an account, or changes how a follow already made shows it: notify and languages (R8) change only
|
||||
/// when given (languages empty: every language).</summary>
|
||||
Task<Following> FollowAs(LocalActor follower, string target, bool showReblogs, CancellationToken token, bool? notify = default,
|
||||
IReadOnlyList<string> languages = default);
|
||||
Task UnfollowAs(LocalActor follower, string target, CancellationToken token);
|
||||
Task<bool> Decide(LocalActor me, string followerAccountId, bool accept, CancellationToken token);
|
||||
Task<int> ResendPending(DateTime now, CancellationToken token);
|
||||
@@ -93,7 +96,18 @@ namespace PrivaPub.Domain.Social
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Following> FollowAs(LocalActor follower, string target, bool showReblogs, CancellationToken token)
|
||||
// a follow's languages as kept: their primary subtags, lower case; none, every language (null)
|
||||
public static List<string> Languages(IReadOnlyList<string> languages)
|
||||
{
|
||||
var kept = (languages ?? Array.Empty<string>())
|
||||
.Select(l => l?.Trim().Replace('_', '-').Split('-')[0].ToLowerInvariant())
|
||||
.Where(l => l is { Length: >= 2 and <= 3 } && l.All(char.IsAsciiLetterLower))
|
||||
.Distinct().Take(20).ToList();
|
||||
return kept.Count == 0 ? null : kept;
|
||||
}
|
||||
|
||||
public async Task<Following> FollowAs(LocalActor follower, string target, bool showReblogs, CancellationToken token, bool? notify = default,
|
||||
IReadOnlyList<string> languages = default)
|
||||
{
|
||||
var (local, remote) = await ResolveTarget(target, token);
|
||||
if (local == default && remote == default)
|
||||
@@ -108,6 +122,17 @@ namespace PrivaPub.Domain.Social
|
||||
if (existing.ShowReblogs != showReblogs)
|
||||
await DB.Default.Update<Following>().MatchID(existing.ID).Modify(f => f.ShowReblogs, showReblogs).ExecuteAsync(token);
|
||||
existing.ShowReblogs = showReblogs;
|
||||
if (notify is { } notifying && existing.Notify != notifying)
|
||||
{
|
||||
await DB.Default.Update<Following>().MatchID(existing.ID).Modify(f => f.Notify, notifying).ExecuteAsync(token);
|
||||
existing.Notify = notifying;
|
||||
}
|
||||
if (languages != default)
|
||||
{
|
||||
var kept = Languages(languages);
|
||||
await DB.Default.Update<Following>().MatchID(existing.ID).Modify(f => f.Languages, kept).ExecuteAsync(token);
|
||||
existing.Languages = kept;
|
||||
}
|
||||
// a request still unanswered is sent again, at most once an hour: a server can take a Follow (202) and
|
||||
// drop it later, as one that cannot yet read our actor does; one that holds it already ignores the copy
|
||||
if (existing.State == FollowState.Requested && remote != default)
|
||||
@@ -125,6 +150,8 @@ namespace PrivaPub.Domain.Social
|
||||
TargetIsLocal = local != default,
|
||||
TargetInboxURL = local?.Inbox ?? remote.InboxURL,
|
||||
ShowReblogs = showReblogs,
|
||||
Notify = notify == true,
|
||||
Languages = Languages(languages),
|
||||
State = local is { ManuallyApprovesFollowers: false } ? FollowState.Accepted : FollowState.Requested
|
||||
};
|
||||
following.ID = (string)following.GenerateNewID();
|
||||
@@ -157,6 +184,8 @@ namespace PrivaPub.Domain.Social
|
||||
|
||||
await DB.Default.DeleteAsync<Following>(following.ID);
|
||||
await DB.Default.DeleteAsync<PersonaListMember>(m => m.AvatarId == follower.Id && m.AccountId == following.TargetAccountId);
|
||||
// an endorsement holds only while the follow does, as on Mastodon (R8)
|
||||
await DB.Default.DeleteAsync<Endorsement>(e => e.AvatarId == follower.Id && e.TargetActorURI == targetUri);
|
||||
if (following.TargetIsLocal)
|
||||
{
|
||||
await DB.Default.DeleteAsync<Follower>(f => f.LocalActorId == following.TargetAccountId && f.ActorURI == follower.Uri);
|
||||
|
||||
Reference in new issue
Block a user