Two owner decisions of 2026-10-05, recorded in the roadmap.
Replies passed on ("the fediverse is broken without"): a public or unlisted
reply from another server to a persona's public, unlisted or followers-only
post goes on to the persona's followers as its author's server sent it, as
Mastodon forwards it, never to the replier's own server, never for a
local-only or group post; its edit and deletion follow. Only an activity its
own actor delivered is passed on (Arrival.Raw), so nothing forwarded is
forwarded again. The town checks it as relay.reply cells (specs/relay-five:
882 checks pass); Mastodon takes a passed-on activity only with an LD
signature, which GoToSocial and Akkoma don't add, and the checker knows it.
Events: a persona joins another server's event with a Join and leaves it with
a Leave, both to the organiser only, through
POST /api/privapub/v1/statuses/:id/join|leave; the organiser's Accept or
Reject is routed by our join id and shows as privapub.event.participation.
Events by invitation or taken on another site are refused before anything
is sent. Mobilizon's scenario joins and leaves an event (28 checks) and keeps
one for decePubClient's e2e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
187 lines
11 KiB
C#
187 lines
11 KiB
C#
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Models.Federation;
|
|
using PrivaPub.Models.Group;
|
|
using PrivaPub.Models.Jobs;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Models.Statistics;
|
|
using PrivaPub.Models.User;
|
|
|
|
namespace PrivaPub.Infrastructure.Data
|
|
{
|
|
public static class Indexes
|
|
{
|
|
public static readonly TimeSpan TombstoneLifetime = TimeSpan.FromDays(90);
|
|
|
|
public static async Task Create(CancellationToken token = default)
|
|
{
|
|
await Unique<Post>(p => p.ObjectURI, Builders<Post>.Filter.Type(p => p.ObjectURI, BsonType.String), token);
|
|
await Plain<Post>(token, p => p.GroupUserId, p => p.ID);
|
|
await Plain<Post>(token, p => p.GroupId, p => p.ID);
|
|
await Plain<Post>(token, p => p.ActorURI);
|
|
await Plain<Post>(token, p => p.ConversationId, p => p.ID);
|
|
await Plain<Post>(token, p => p.InReplyToURI);
|
|
await DB.Default.Index<Post>().Key(p => p.Geo, KeyType.Geo2DSphere).CreateAsync(token);
|
|
|
|
await Unique<DmPost>(p => p.ObjectURI, Builders<DmPost>.Filter.Type(p => p.ObjectURI, BsonType.String), token);
|
|
await Plain<DmPost>(token, p => p.GroupId, p => p.ID);
|
|
|
|
await Unique<ForeignAvatar>(a => a.ActorURI, Builders<ForeignAvatar>.Filter.Type(a => a.ActorURI, BsonType.String), token);
|
|
await Plain<ForeignAvatar>(token, a => a.PublicKeyId);
|
|
|
|
await DB.Default.Index<Follower>()
|
|
.Key(f => f.LocalActorId, KeyType.Ascending)
|
|
.Key(f => f.LocalActorKind, KeyType.Ascending)
|
|
.Key(f => f.ActorURI, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Plain<Follower>(token, f => f.ActorURI);
|
|
|
|
await DB.Default.Index<RootToAvatar>()
|
|
.Key(r => r.RootId, KeyType.Ascending)
|
|
.Key(r => r.AvatarId, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Plain<RootToAvatar>(token, r => r.AvatarId);
|
|
|
|
await Unique<ReservedName>(r => r.Name, Builders<ReservedName>.Filter.Type(r => r.Name, BsonType.String), token);
|
|
await Unique<RootUser>(u => u.UserName, Builders<RootUser>.Filter.Type(u => u.UserName, BsonType.String), token);
|
|
await Plain<Avatar>(token, a => a.UserName);
|
|
await Plain<Group>(token, g => g.UserName);
|
|
await Plain<Group>(token, g => g.InvitationCode);
|
|
|
|
await Plain<DmGroup>(token, g => g.ParticipantsKey);
|
|
await Plain<DmGroup>(token, g => g.LastPostId);
|
|
await DB.Default.Index<ConversationState>()
|
|
.Key(s => s.AvatarId, KeyType.Ascending)
|
|
.Key(s => s.ConversationId, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Unique<GroupAnnouncement>(a => a.ActivityURI, Builders<GroupAnnouncement>.Filter.Type(a => a.ActivityURI, BsonType.String), token);
|
|
|
|
await Plain<Delivery>(token, d => d.DeliveredAt, d => d.AbandonedAt, d => d.NextAttemptAt);
|
|
|
|
await Plain<Job>(token, j => j.Kind, j => j.State, j => j.RunAt);
|
|
await Plain<Job>(token, j => j.State, j => j.LeasedUntil);
|
|
await Unique<Job>(j => j.DedupeKey, Builders<Job>.Filter.Type(j => j.DedupeKey, BsonType.String), token);
|
|
await DB.Default.Index<Job>()
|
|
.Key(j => j.FinishedAt, KeyType.Ascending)
|
|
.Option(o => o.ExpireAfter = TimeSpan.FromDays(7))
|
|
.CreateAsync(token);
|
|
await DB.Default.Index<Following>()
|
|
.Key(f => f.AvatarId, KeyType.Ascending)
|
|
.Key(f => f.TargetActorURI, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Plain<Following>(token, f => f.TargetActorURI, f => f.State);
|
|
await Plain<Following>(token, f => f.TargetAccountId, f => f.State);
|
|
await Plain<Following>(token, f => f.FollowActivityURI);
|
|
await DB.Default.Index<TimelineEntry>()
|
|
.Key(e => e.AvatarId, KeyType.Ascending)
|
|
.Key(e => e.PostId, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Plain<TimelineEntry>(token, e => e.PostId);
|
|
await Unique<Notification>(n => n.DedupeKey, Builders<Notification>.Filter.Type(n => n.DedupeKey, BsonType.String), token);
|
|
await Plain<Notification>(token, n => n.AvatarId, n => n.ID);
|
|
await DB.Default.Index<Favourite>()
|
|
.Key(f => f.AccountId, KeyType.Ascending)
|
|
.Key(f => f.PostId, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Plain<Favourite>(token, f => f.ActivityURI);
|
|
await DB.Default.Index<Participation>()
|
|
.Key(p => p.AvatarId, KeyType.Ascending)
|
|
.Key(p => p.PostId, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
await Plain<Participation>(token, p => p.ActivityURI);
|
|
await DB.Default.Index<Marker>()
|
|
.Key(m => m.AvatarId, KeyType.Ascending)
|
|
.Key(m => m.Timeline, KeyType.Ascending)
|
|
.Option(o => o.Unique = true)
|
|
.CreateAsync(token);
|
|
foreach (var pair in new (Func<Task>, string)[]
|
|
{
|
|
(() => DB.Default.Index<Block>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.TargetActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "block"),
|
|
(() => DB.Default.Index<FollowedTag>().Key(t => t.AvatarId, KeyType.Ascending).Key(t => t.Name, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "followed tag"),
|
|
(() => DB.Default.Index<PersonaListMember>().Key(m => m.ListId, KeyType.Ascending).Key(m => m.AccountId, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "list member"),
|
|
(() => DB.Default.Index<BlockedBy>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.ActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "blocked by"),
|
|
(() => DB.Default.Index<Mute>().Key(m => m.AvatarId, KeyType.Ascending).Key(m => m.TargetActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "mute"),
|
|
(() => DB.Default.Index<AccountDomainBlock>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.Domain, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "domain block"),
|
|
(() => DB.Default.Index<Bookmark>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.PostId, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "bookmark"),
|
|
(() => DB.Default.Index<Pin>().Key(p => p.AvatarId, KeyType.Ascending).Key(p => p.PostId, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "pin")
|
|
})
|
|
await pair.Item1();
|
|
await Plain<Block>(token, b => b.TargetActorURI);
|
|
await Plain<BlockedBy>(token, b => b.ActorURI);
|
|
await Plain<PersonaList>(token, l => l.AvatarId);
|
|
await Plain<PersonaFilter>(token, f => f.AvatarId);
|
|
await Plain<FollowedTag>(token, t => t.Name);
|
|
await Plain<Post>(token, p => p.Tags);
|
|
//the words status search reads, in every language alike (no stemming, no stop words); Language is not the
|
|
//index's language field
|
|
await DB.Default.Index<Post>().Key(p => p.Text, KeyType.Text).Key(p => p.ContentHtml, KeyType.Text).Key(p => p.SpoilerText, KeyType.Text)
|
|
.Key(p => p.Title, KeyType.Text).Option(o => { o.DefaultLanguage = "none"; o.LanguageOverride = "textSearchLanguage"; o.Name = "words"; })
|
|
.CreateAsync(token);
|
|
await Plain<PersonaListMember>(token, m => m.AvatarId);
|
|
await Plain<Mute>(token, m => m.TargetActorURI);
|
|
await Plain<Report>(token, r => r.IsResolved, r => r.ID);
|
|
await Unique<DomainBlock>(b => b.Domain, Builders<DomainBlock>.Filter.Type(b => b.Domain, BsonType.String), token);
|
|
await Unique<RemoteInstance>(i => i.Host, Builders<RemoteInstance>.Filter.Type(i => i.Host, BsonType.String), token);
|
|
await Unique<ObjectRecord>(r => r.ObjectURI, Builders<ObjectRecord>.Filter.Type(r => r.ObjectURI, BsonType.String), token);
|
|
await Plain<ObjectRecord>(token, r => r.PostId);
|
|
await Unique<DeletedObject>(d => d.ObjectURI, Builders<DeletedObject>.Filter.Type(d => d.ObjectURI, BsonType.String), token);
|
|
await DB.Default.Index<DeletedObject>().Key(d => d.DeletedAt, KeyType.Ascending).Option(o => o.ExpireAfter = TombstoneLifetime).CreateAsync(token);
|
|
await DB.Default.Index<Downvote>().Key(d => d.PostId, KeyType.Ascending).Key(d => d.ActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token);
|
|
await Plain<Downvote>(token, d => d.ActivityURI);
|
|
await DB.Default.Index<PollVote>().Key(v => v.PostId, KeyType.Ascending).Key(v => v.ActorURI, KeyType.Ascending).Key(v => v.Choice, KeyType.Ascending)
|
|
.Option(o => o.Unique = true).CreateAsync(token);
|
|
await DB.Default.Index<PollVote>().Key(v => v.PostId, KeyType.Ascending).Key(v => v.VoterAccountId, KeyType.Ascending).CreateAsync(token);
|
|
await DB.Default.Index<Reaction>().Key(r => r.PostId, KeyType.Ascending).Key(r => r.ActorURI, KeyType.Ascending).Key(r => r.Emoji, KeyType.Ascending)
|
|
.Option(o => o.Unique = true).CreateAsync(token);
|
|
await Plain<Reaction>(token, r => r.ActivityURI);
|
|
await DB.Default.Index<QuoteLicence>().Key(l => l.PostId, KeyType.Ascending).Key(l => l.QuotingObjectURI, KeyType.Ascending).CreateAsync(token);
|
|
await Unique<Domain.Content.LinkPreview>(p => p.Url, Builders<Domain.Content.LinkPreview>.Filter.Type(p => p.Url, BsonType.String), token);
|
|
|
|
await DB.Default.Index<InteractionEvent>().Key(e => e.At, KeyType.Ascending).Option(o => o.ExpireAfter = InteractionEvent.Retention).CreateAsync(token);
|
|
await Plain<InteractionEvent>(token, e => e.Host, e => e.At);
|
|
await DB.Default.Index<InteractionSalt>().Key(s => s.Day, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token);
|
|
await DB.Default.Index<InteractionSalt>().Key(s => s.ExpiresAt, KeyType.Ascending).Option(o => o.ExpireAfter = TimeSpan.Zero).CreateAsync(token);
|
|
await DB.Default.Index<InstanceDay>().Key(d => d.Day, KeyType.Ascending).Key(d => d.Host, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token);
|
|
await Plain<InstanceDay>(token, d => d.Host, d => d.Day);
|
|
await DB.Default.Index<ServerDay>().Key(d => d.Day, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token);
|
|
await DB.Default.Index<RemoteInstanceSnapshot>().Key(s => s.Host, KeyType.Ascending).Key(s => s.Week, KeyType.Ascending)
|
|
.Option(o => o.Unique = true).CreateAsync(token);
|
|
await Plain<RemoteInstanceSnapshot>(token, s => s.Week);
|
|
await Plain<RemoteInstanceSnapshot>(token, s => s.CdnDomain, s => s.Week);
|
|
await Plain<RemoteInstanceSnapshot>(token, s => s.Cdn, s => s.Week);
|
|
await Plain<CdnRangeSet>(token, s => s.Key);
|
|
await Plain<RemoteInstance>(token, i => i.Seen);
|
|
await Plain<RemoteInstance>(token, i => i.LastCrawledAt);
|
|
}
|
|
|
|
static async Task Unique<T>(System.Linq.Expressions.Expression<Func<T, object>> key, FilterDefinition<T> partial,
|
|
CancellationToken token) where T : IEntity =>
|
|
await DB.Default.Index<T>()
|
|
.Key(key, KeyType.Ascending)
|
|
.Option(o =>
|
|
{
|
|
o.Unique = true;
|
|
o.PartialFilterExpression = partial;
|
|
})
|
|
.CreateAsync(token);
|
|
|
|
static async Task Plain<T>(CancellationToken token, params System.Linq.Expressions.Expression<Func<T, object>>[] keys) where T : IEntity
|
|
{
|
|
var index = DB.Default.Index<T>();
|
|
foreach (var key in keys)
|
|
index.Key(key, KeyType.Ascending);
|
|
await index.CreateAsync(token);
|
|
}
|
|
}
|
|
}
|