R5 the server's own settings: instance, rules, announcements, relays, media limits, the staff's log
Build / Build (push) Successful in 1m3s

A singleton ServerSettings (title, descriptions, contact, thumbnail, privacy policy, terms, rules, relays, media
overrides, PublishActivity), cached by InstanceSettings and read by the instance API, NodeInfo, the instance actor, the
web pages and the mailer; its defaults are the old literals, so nothing changes until an administrator edits.
/clientapi/admin/settings (and /thumbnail), /announcements, /relays for administrators; /clientapi/moderator/log, the
staff's log (AdminAction, written by domain blocks, backups, settings and the CLI; no IP, never a root paired with its
personas). The Mastodon API's rules, extended description, privacy policy, terms of service, domain blocks (Published,
Obfuscate), announcements (dismiss, reactions, streaming) and activity (only when published). Reports carry rule ids.
Media overrides clamped to hard caps. Relays named by the settings reconcile next to the configured ones.

Verified: interop.sh instance accounts relay with mastodon, relay, aoderelay and mailpit, 119 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 04:02:47 +02:00
1 parent f67e2ae35e
commit 70d1ffe9a2
46 files changed
+2223 -107

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
using MongoDB.Entities;
namespace PrivaPub.Models
{
// One act of the server's staff, kept for the moderators' log (Domain/Moderation/ModerationLog.cs, completion program
// R5): who (an administrator's or moderator's root id, or "cli" for the command line), what, and on what. Never an IP
// address, and never a root next to its own personas: an entry about a persona drops the acting root when the persona
// is that root's, and an entry about a root names none of its personas. A backup holds the log and a restore keeps
// every entry made since (ProtectiveMerge).
public class AdminAction : Entity
{
public const string Cli = "cli";
public DateTime At { get; set; } = DateTime.UtcNow;
public string ByRootId { get; set; }
public string Action { get; set; }//AdminActions.*
public string TargetKind { get; set; }//AdminTargets.*
public string TargetId { get; set; }
public string TargetLabel { get; set; }
public string ReportId { get; set; }
public string Note { get; set; }
}
public static class AdminActions
{
public const string DomainBlockInsert = "domain_block.insert";
public const string DomainBlockDelete = "domain_block.delete";
public const string BackupCreate = "backup.create";
public const string BackupDelete = "backup.delete";
public const string BackupRestore = "backup.restore";
public const string SettingsUpdate = "settings.update";
public const string SettingsThumbnail = "settings.thumbnail";
public const string RuleInsert = "rule.insert";
public const string RuleUpdate = "rule.update";
public const string RuleDelete = "rule.delete";
public const string AnnouncementInsert = "announcement.insert";
public const string AnnouncementUpdate = "announcement.update";
public const string AnnouncementDelete = "announcement.delete";
public const string RelayInsert = "relay.insert";
public const string RelayDelete = "relay.delete";
public const string ReportResolve = "report.resolve";
public const string RootPromote = "root.promote";
public const string RootDemote = "root.demote";
public const string RootCreate = "root.create";
public const string RootDelete = "root.delete";
public const string RootResetTotp = "root.reset_totp";
public const string PersonaMove = "persona.move";
}
public static class AdminTargets
{
public const string Root = "root";
public const string Persona = "persona";
public const string Account = "account";
public const string Domain = "domain";
public const string Report = "report";
public const string Settings = "settings";
public const string Rule = "rule";
public const string Announcement = "announcement";
public const string Emoji = "emoji";
public const string Relay = "relay";
public const string Backup = "backup";
}
}
@@ -9,6 +9,10 @@ namespace PrivaPub.Models.Federation
public bool RejectMedia { get; set; }
public string PublicComment { get; set; }
public string PrivateComment { get; set; }
// listed at /api/v1/instance/domain_blocks, which tells the blocked server too (off by default); Obfuscate hides part
// of its name there, as Mastodon does
public bool Published { get; set; }
public bool Obfuscate { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
+1
View File
@@ -24,6 +24,7 @@ namespace PrivaPub.Models.Media
public DateTime? AttachedAt { get; set; }
public string ScheduledStatusId { get; set; }//kept for a scheduled post until it is published or dropped
public string ProfileOfAvatarId { get; set; }//a persona's avatar or header (Kind "avatar" or "header"), never a post's
public string ServerUse { get; set; }//the server's own picture ("thumbnail", ServerSettings.ThumbnailMediaId): held while it is set
// set once nothing holds it any more (its post deleted, edited out, replaced, its root removed, never posted): its
// files move out of what /media/files serves at once, and the janitor deletes them after a grace
public DateTime? TrashedAt { get; set; }
+65
View File
@@ -0,0 +1,65 @@
using MongoDB.Entities;
namespace PrivaPub.Models
{
// The server's own settings (completion program R5), edited from the administrator's page: one document with the fixed
// id `settings`, read through IInstanceSettings (Domain/Instance/InstanceSettings.cs). While none exists, Defaults() is
// what PrivaPub served and sent before the settings existed, word for word, so nothing changes until an administrator
// saves. A backup holds it, and a restore keeps the live one (ProtectiveMerge).
public class ServerSettings : Entity
{
public const string Name = "settings";
public const string DefaultTitle = "PrivaPub";
public const string DefaultDescription = "A small ActivityPub server where one private login keeps several unlinkable public personas.";
public const string DefaultThumbnailPath = "/media/missing-header.png";
public static readonly DateTime DefaultPrivacyPolicyUpdatedAt = new(2026, 10, 4, 0, 0, 0, DateTimeKind.Utc);
// the privacy policy PrivaPub served before it could be edited, as Markdown: it renders to the same paragraph
public const string DefaultPrivacyPolicy = "One private login owns several public personas, and nobody but this server's admin can tell they belong "
+ "together. What a persona posts is shared as its visibility says; a located post never leaves this server. "
+ "Uploads lose their metadata. Statistics name servers, never people: see /stargazing.";
public string Title { get; set; } = DefaultTitle;
public string ShortDescription { get; set; } = DefaultDescription;
public string Description { get; set; } = DefaultDescription;//Markdown
public DateTime? DescriptionUpdatedAt { get; set; }//null: the description was never edited (it then answers today)
public string ContactEmail { get; set; }
public string ContactAccountId { get; set; }//a local persona the administrator chose: publicly tied to running the server
public string ThumbnailMediaId { get; set; }//its MediaAttachment (ServerUse "thumbnail")
public string ThumbnailUrl { get; set; }//null: /media/missing-header.png
public string PrivacyPolicy { get; set; } = DefaultPrivacyPolicy;//Markdown
public DateTime? PrivacyPolicyUpdatedAt { get; set; } = DefaultPrivacyPolicyUpdatedAt;
public string TermsOfService { get; set; }//Markdown; null: the server has none (the API answers 404)
public DateTime? TermsEffectiveAt { get; set; }
public List<ServerRule> Rules { get; set; } = new();//soft-deleted ones stay, so old reports keep their meaning
public List<string> Relays { get; set; } = new();//besides Federation:Relays
public ServerMediaLimits Media { get; set; } = new();
public List<string> StaffPicks { get; set; } = new();//account ids, for later
public bool PublishActivity { get; set; }//GET /api/v1/instance/activity; off by default
public DateTime? UpdatedAt { get; set; }//null: never saved
public string UpdatedBy { get; set; }//the administrator's root id, or "cli"
public static ServerSettings Defaults() => new() { ID = Name };
/// <summary>The rules shown to everyone: those not deleted, in their order.</summary>
public IEnumerable<ServerRule> ListedRules() => Rules.Where(r => r.DeletedAt == null).OrderBy(r => r.Position);
}
public class ServerRule
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public string Text { get; set; }
public string Hint { get; set; }
public int Position { get; set; }
public DateTime? DeletedAt { get; set; }
}
// what the administrator changed of Media:* (MediaOptions); null keeps the configured value. Applied by
// Domain/Instance/MediaLimitsFromSettings within hard caps.
public class ServerMediaLimits
{
public long? QuotaBytesPerRoot { get; set; }
public long? MaxImageBytes { get; set; }
public long? MaxVideoBytes { get; set; }
public int? MaxSeconds { get; set; }
}
}
+36
View File
@@ -0,0 +1,36 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Social
{
// An announcement of the server's administrators (Mastodon's /api/v1/announcements, completion program R5): written in
// Markdown on the administrator's page, shown while published and not ended. Never federated.
public class Announcement : Entity
{
public string Text { get; set; }//Markdown
public string Html { get; set; }//rendered and sanitized (ContentRenderer.Document)
public DateTime? StartsAt { get; set; }//what it is about, if it has a time (as Mastodon's)
public DateTime? EndsAt { get; set; }//once past, it is no longer shown
public bool AllDay { get; set; }
public bool IsPublished { get; set; }
public DateTime? PublishedAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
// a persona dismissed it (Mastodon's read), kept per persona and never told anyone
public class AnnouncementRead : Entity
{
public string AnnouncementId { get; set; }
public string AvatarId { get; set; }
public DateTime ReadAt { get; set; } = DateTime.UtcNow;
}
// a persona's reaction to it (one unicode emoji), counted for everyone, the persona's own known only to it
public class AnnouncementReaction : Entity
{
public string AnnouncementId { get; set; }
public string AvatarId { get; set; }
public string Name { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}
+1
View File
@@ -72,6 +72,7 @@ namespace PrivaPub.Models.Social
public List<string> PostIds { get; set; } = new();
public List<string> ObjectURIs { get; set; } = new();
public string Category { get; set; } = "other";
public List<string> RuleIds { get; set; } = new();//the server's rules it says were broken (ServerSettings.Rules, deleted ones too)
public string Comment { get; set; }
public bool Forwarded { get; set; }
public bool IsResolved { get; set; }