One Post model for every visibility, read from notes the way peers write them
Post gains what federation and the Mastodon API need: Visibility (Public, Unlisted, FollowersOnly, Direct, Circle, LocalGeo), the author's account id, to/cc, the Create's id, url, context, quote, InReplyToURI and the parent's author, a separate SpoilerText next to the title, language, mentions, hashtags, remote attachments (alt text, blurhash, focus, size), reply/favourite/reblog counters, revisions, EditedAt and DeletedAt. Direct messages are Posts with Visibility Direct and a ConversationId; migration _005 copies DmPost rows across with their ids and fills the new fields of existing posts. DmPost is left in place so a rollback still sees the old messages. Inbound: - NoteParser reads Note, Article, Page, Question and media types: content, then contentMap, then _misskey_content; summary as the spoiler and name as the title; Mention and Hashtag tags; attachments; a PeerTube-style list attribution prefers the person over the channel; quote URIs. - Addressing classifies like Mastodon, finding followers-only by the author's own followers URL (now stored on ForeignAvatar), not by a "/followers" suffix. - Create keeps a post when a local persona is addressed or mentioned, when it replies to a local post (the parent's reply count goes up) or when a community it follows is addressed; an unsolicited public post is not stored. Update keeps the previous version as a revision. The outbox and object endpoints serve only Public and Unlisted posts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
796f06acf4
commit
e9529310fe
21 files changed
+777
-192
No files matched your search
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
@@ -51,12 +52,12 @@ namespace PrivaPub.Federation.Controllers
|
||||
return NotFound();
|
||||
|
||||
var total = local.Kind == LocalActorKind.Group
|
||||
? await DB.Default.CountAsync<PostEntity>(p => p.GroupId == local.Id && !p.IsLocalOnly, token)
|
||||
: await DB.Default.CountAsync<PostEntity>(p => p.GroupUserId == local.Id && !p.IsFederatedCopy && !p.IsLocalOnly, token);
|
||||
? await DB.Default.CountAsync<PostEntity>(p => p.GroupId == local.Id && (p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted), token)
|
||||
: await DB.Default.CountAsync<PostEntity>(p => p.GroupUserId == local.Id && !p.IsFederatedCopy && (p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted), token);
|
||||
|
||||
var latest = await (local.Kind == LocalActorKind.Group
|
||||
? _dbEntities.Posts.Match(p => p.GroupId == local.Id && !p.IsLocalOnly)
|
||||
: _dbEntities.Posts.Match(p => p.GroupUserId == local.Id && !p.IsFederatedCopy && !p.IsLocalOnly))
|
||||
? _dbEntities.Posts.Match(p => p.GroupId == local.Id && (p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted))
|
||||
: _dbEntities.Posts.Match(p => p.GroupUserId == local.Id && !p.IsFederatedCopy && (p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted)))
|
||||
.Sort(p => p.CreationDate, Order.Descending)
|
||||
.Limit(OutboxSize)
|
||||
.ExecuteAsync(token);
|
||||
@@ -70,7 +71,7 @@ namespace PrivaPub.Federation.Controllers
|
||||
continue;
|
||||
}
|
||||
var group = string.IsNullOrEmpty(post.GroupId) ? default : await _localActors.FindById(LocalActorKind.Group, post.GroupId, token);
|
||||
var note = ActivityPubRenderer.Note(post, local, group, default);
|
||||
var note = ActivityPubRenderer.Note(post, local, group, post.InReplyToURI);
|
||||
items.Add(ActivityPubRenderer.Create(local, note, $"create-{post.ID}"));
|
||||
}
|
||||
|
||||
@@ -103,14 +104,13 @@ namespace PrivaPub.Federation.Controllers
|
||||
return NotFound();
|
||||
|
||||
var post = await _dbEntities.Posts
|
||||
.Match(p => p.ID == postId && p.GroupUserId == local.Id && !p.IsFederatedCopy && !p.IsLocalOnly)
|
||||
.Match(p => p.ID == postId && p.GroupUserId == local.Id && !p.IsFederatedCopy && (p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted))
|
||||
.ExecuteFirstAsync(token);
|
||||
if (post == default)
|
||||
return NotFound();
|
||||
|
||||
var group = string.IsNullOrEmpty(post.GroupId) ? default : await _localActors.FindById(LocalActorKind.Group, post.GroupId, token);
|
||||
var replyTo = await ReplyTarget(post.AnsweringToPostId, token);
|
||||
var note = ActivityPubRenderer.Note(post, local, group, replyTo);
|
||||
var note = ActivityPubRenderer.Note(post, local, group, post.InReplyToURI);
|
||||
note["@context"] = ActivityPubRenderer.ActivityStreams;
|
||||
return Activity(note);
|
||||
}
|
||||
@@ -132,15 +132,6 @@ namespace PrivaPub.Federation.Controllers
|
||||
public async Task<IActionResult> SharedInbox(CancellationToken token) =>
|
||||
Answer(await _inbox.Receive(Request, default, token));
|
||||
|
||||
async Task<string> ReplyTarget(string answeringToPostId, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(answeringToPostId))
|
||||
return default;
|
||||
if (answeringToPostId.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
return answeringToPostId;
|
||||
return (await _dbEntities.Posts.MatchID(answeringToPostId).ExecuteFirstAsync(token))?.ObjectURI;
|
||||
}
|
||||
|
||||
IActionResult Answer(InboxResult result)
|
||||
{
|
||||
if (result.Error != default)
|
||||
|
||||
Reference in new issue
Block a user