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
@@ -35,6 +35,8 @@ namespace PrivaPub.Domain.Timelines
|
||||
var recipients = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (!post.IsFederatedCopy && !string.IsNullOrEmpty(post.GroupUserId))
|
||||
recipients.Add(post.GroupUserId);
|
||||
// those following the author with notify (R8), told of the post once it reached their home
|
||||
var notifying = new List<string>();
|
||||
|
||||
switch (post.Visibility)
|
||||
{
|
||||
@@ -44,7 +46,11 @@ namespace PrivaPub.Domain.Timelines
|
||||
: await _dbEntities.Followings.Match(f => f.TargetAccountId == post.GroupUserId && f.TargetIsLocal && f.State == FollowState.Accepted).ExecuteAsync(token);
|
||||
foreach (var following in followers)
|
||||
if (await Shows(following, post, token))
|
||||
{
|
||||
recipients.Add(following.AvatarId);
|
||||
if (following.Notify && Notifies(post))
|
||||
notifying.Add(following.AvatarId);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(post.AudienceURI))
|
||||
// (the server's reader of feeds follows communities for nobody's home)
|
||||
foreach (var member in await _dbEntities.Followings.Match(f => f.TargetActorURI == post.AudienceURI && f.State == FollowState.Accepted && f.FollowerKind == null).ExecuteAsync(token))
|
||||
@@ -100,6 +106,8 @@ namespace PrivaPub.Domain.Timelines
|
||||
if (string.IsNullOrEmpty(post.ReblogOfPostId))
|
||||
foreach (var mention in post.Mentions.Where(m => m.IsLocal && !string.IsNullOrEmpty(m.AccountId)))
|
||||
await Notifications.Add(mention.AccountId, NotificationType.Mention, post.AuthorAccountId ?? post.GroupUserId, post.ActorURI, post.ID, token);
|
||||
foreach (var avatarId in notifying.Where(recipients.Contains).Distinct())
|
||||
await Notifications.Add(avatarId, NotificationType.Status, post.AuthorAccountId ?? post.GroupUserId, post.ActorURI, post.ID, token);
|
||||
if (Streams.Anyone)
|
||||
await Stream(post, recipients, token);
|
||||
}
|
||||
@@ -186,10 +194,36 @@ namespace PrivaPub.Domain.Timelines
|
||||
}
|
||||
}
|
||||
|
||||
// Mastodon's notify: the account's own posts and its replies to itself, never its boosts (FeedInsertWorker)
|
||||
static bool Notifies(Post post) =>
|
||||
string.IsNullOrEmpty(post.ReblogOfPostId) && post.Visibility is PostVisibility.Public or PostVisibility.Unlisted or PostVisibility.FollowersOnly
|
||||
&& (string.IsNullOrEmpty(post.InReplyToURI) || post.InReplyToAccountId == (post.AuthorAccountId ?? post.GroupUserId));
|
||||
|
||||
// a follow's languages (R8): a post in another language stays out of that home, as on Mastodon; one whose language
|
||||
// is not known comes in
|
||||
static bool InLanguages(Following following, Post post) =>
|
||||
following.Languages is not { Count: > 0 } || string.IsNullOrEmpty(post.Language)
|
||||
|| following.Languages.Contains(post.Language.Replace('_', '-').Split('-')[0].ToLowerInvariant());
|
||||
|
||||
// a direct conversation that has a new post, to the `direct` streams of the personas taking part in it (R8), as
|
||||
// Mastodon's `conversation` event: each connection maps it for its own persona when it sends it
|
||||
public static async Task Conversation(string conversationId, CancellationToken token)
|
||||
{
|
||||
if (!Streams.Anyone || string.IsNullOrEmpty(conversationId))
|
||||
return;
|
||||
var conversation = await DB.Default.Find<DmGroup>().MatchID(conversationId).ExecuteFirstAsync(token);
|
||||
var told = new Streams.Event("conversation", conversationId);
|
||||
foreach (var member in conversation?.Members.Where(m => !m.IsForeign).Select(m => m.AvatarId).Distinct() ?? Enumerable.Empty<string>())
|
||||
if (Streams.Listening(member))
|
||||
Streams.ToPersona(member, new Streams.Key(Streams.Direct), told);
|
||||
}
|
||||
|
||||
async Task<bool> Shows(Following following, Post post, CancellationToken token)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(post.ReblogOfPostId))
|
||||
return following.ShowReblogs;
|
||||
if (!InLanguages(following, post))
|
||||
return false;
|
||||
var repliedTo = post.InReplyToAccountId;
|
||||
var author = post.AuthorAccountId ?? post.GroupUserId;
|
||||
if (string.IsNullOrEmpty(post.InReplyToURI) || repliedTo == author || repliedTo == following.AvatarId)
|
||||
|
||||
Reference in new issue
Block a user