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

@@ -12,6 +12,9 @@ namespace PrivaPub.ClientModels.Admin
public bool Suspend { get; set; } = true;
public bool RejectMedia { get; set; }
// listed at /api/v1/instance/domain_blocks (off by default: it tells the blocked server, and everyone, of the block)
public bool Published { get; set; } = false;
public bool Obfuscate { get; set; } = false;//listed with part of its name hidden
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string PublicComment { get; set; }
@@ -28,6 +31,8 @@ namespace PrivaPub.ClientModels.Admin
public bool RejectMedia { get; set; }
public string PublicComment { get; set; }
public string PrivateComment { get; set; }
public bool Published { get; set; } = false;
public bool Obfuscate { get; set; } = false;
public DateTime CreatedAt { get; set; }
}
}
@@ -0,0 +1,26 @@
namespace PrivaPub.ClientModels.Admin
{
// the staff's log, newest first (GET /clientapi/moderator/log?before=&limit=): Next is the `before` of the next page,
// null on the last
public class ViewAdminActionsPage
{
public List<ViewAdminAction> Items { get; set; } = [];
public string Next { get; set; }
}
public class ViewAdminAction
{
public string Id { get; set; }
public DateTime At { get; set; }
// who acted: a login name (to administrators; to a moderator only its own), "cli" for the command line, null when not
// told (an act on a persona of the acting login keeps nobody's name)
public string By { get; set; }
public bool ByYou { get; set; } = false;
public string Action { get; set; }//e.g. domain_block.insert, settings.update, report.resolve, root.promote
public string TargetKind { get; set; }//root, persona, account, domain, report, settings, rule, announcement, emoji, relay, backup
public string TargetId { get; set; }
public string TargetLabel { get; set; }
public string ReportId { get; set; }
public string Note { get; set; }
}
}
@@ -0,0 +1,47 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Admin
{
// an announcement as the administrator's page shows it (/clientapi/admin/announcements/list): its Markdown and what it
// renders to, whether it is published and shown now, how many personas dismissed it and its reactions
public class ViewAnnouncement
{
public string Id { get; set; }
public string Text { get; set; }
public string Html { get; set; }
public DateTime? StartsAt { get; set; }
public DateTime? EndsAt { get; set; }
public bool AllDay { get; set; } = false;
public bool IsPublished { get; set; } = false;
public bool IsShown { get; set; } = false;//published and not ended
public DateTime? PublishedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public long Reads { get; set; } = 0;
public List<ViewAnnouncementReaction> Reactions { get; set; } = [];
}
public class ViewAnnouncementReaction
{
public string Name { get; set; }
public long Count { get; set; } = 0;
}
// insert (no Id) and update (its Id)
public class AnnouncementForm
{
[StringLength(64, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Id { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(5000, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Text { get; set; }//Markdown
public DateTime? StartsAt { get; set; }
public DateTime? EndsAt { get; set; }//once past, it is no longer shown
public bool AllDay { get; set; } = false;
public bool IsPublished { get; set; } = false;
}
}
@@ -11,6 +11,8 @@ namespace PrivaPub.ClientModels.Admin
public List<string> PostIds { get; set; } = new();
public List<string> ObjectURIs { get; set; } = new();
public string Category { get; set; }
public List<string> RuleIds { get; set; } = [];//the server's rules it says were broken
public List<string> Rules { get; set; } = [];//their texts, in the same order (a deleted rule's too)
public string Comment { get; set; }
public bool Forwarded { get; set; }
public bool IsResolved { get; set; }
@@ -0,0 +1,167 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Admin
{
// The server's own settings as the administrator's page shows them (GET /clientapi/admin/settings): what the instance
// API, NodeInfo, the instance actor, the web pages and the emails say of the server, its rules, its relays, its media
// limits next to the configured ones, and whether its weekly activity is published. Registrations are read-only: they
// stay in the configuration (Registrations:Mode).
public class ViewServerSettings
{
public string Title { get; set; }
public string ShortDescription { get; set; }
public string Description { get; set; }//Markdown
public DateTime? DescriptionUpdatedAt { get; set; }
public string ContactEmail { get; set; }
public string ContactAccountId { get; set; }
public ViewContactAccount ContactAccount { get; set; }//null when none is set, or it is gone
public string ThumbnailUrl { get; set; }//what the instance API shows (the default picture when none was uploaded)
public bool ThumbnailIsDefault { get; set; } = true;
public string PrivacyPolicy { get; set; }//Markdown
public DateTime? PrivacyPolicyUpdatedAt { get; set; }
public string TermsOfService { get; set; }//Markdown; null: none
public DateTime? TermsEffectiveAt { get; set; }
public List<ViewServerRule> Rules { get; set; } = [];//deleted ones too (IsDeleted), as old reports name them
public List<ViewRelay> Relays { get; set; } = [];
public ViewMediaLimits Media { get; set; } = new();
public string RegistrationMode { get; set; }//"Invitations" or "Open", from the configuration
public bool PublishActivity { get; set; } = false;
public DateTime? UpdatedAt { get; set; }//null: never saved, every value is the default
public string UpdatedBy { get; set; }//the administrator's login, or "cli"
}
public class ViewContactAccount
{
public string Id { get; set; }
public string UserName { get; set; }
public string Name { get; set; }
public string Handle { get; set; }//user@host
}
public class ViewServerRule
{
public string Id { get; set; }
public string Text { get; set; }
public string Hint { get; set; }
public int Position { get; set; } = 0;
public bool IsDeleted { get; set; } = false;
public DateTime? DeletedAt { get; set; }
}
// the administrator's overrides (null: the configured value holds), what the configuration says, and the hard caps
public class ViewMediaLimits
{
public long? QuotaBytesPerRoot { get; set; }
public long? MaxImageBytes { get; set; }
public long? MaxVideoBytes { get; set; }
public int? MaxSeconds { get; set; }
public long ConfiguredQuotaBytesPerRoot { get; set; } = 0;//0: no quota
public long ConfiguredMaxImageBytes { get; set; } = 0;
public long ConfiguredMaxVideoBytes { get; set; } = 0;
public int ConfiguredMaxSeconds { get; set; } = 0;
public long EffectiveQuotaBytesPerRoot { get; set; } = 0;
public long EffectiveMaxImageBytes { get; set; } = 0;
public long EffectiveMaxVideoBytes { get; set; } = 0;
public int EffectiveMaxSeconds { get; set; } = 0;
public long HardMaxImageBytes { get; set; } = ServerMediaLimitsForm.HardMaxImageBytes;
public long HardMaxVideoBytes { get; set; } = ServerMediaLimitsForm.HardMaxVideoBytes;
public int HardMaxSeconds { get; set; } = ServerMediaLimitsForm.HardMaxSeconds;
}
// POST /clientapi/admin/settings: everything the page edits at once (relays and the thumbnail have their own routes).
// A rule keeps its id when the form names it; one without an id is new; one left out is deleted (kept, marked).
public class ServerSettingsForm
{
public const int MaxRules = 50;
public const int MaxText = 20000;
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(64, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = "Title", ResourceType = typeof(FieldsNameResource))]
public string Title { get; set; }
[StringLength(500, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string ShortDescription { get; set; }
[StringLength(MaxText, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = "Description", ResourceType = typeof(FieldsNameResource))]
public string Description { get; set; }
[EmailAddress(ErrorMessageResourceName = "InvalidEmail", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(254, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = "Email", ResourceType = typeof(FieldsNameResource))]
public string ContactEmail { get; set; }
[StringLength(64, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string ContactAccountId { get; set; }//a local persona's account id (its Mastodon id), or none
[StringLength(MaxText, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string PrivacyPolicy { get; set; }
[StringLength(MaxText, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string TermsOfService { get; set; }//empty: none
public DateTime? TermsEffectiveAt { get; set; }//a date; today when terms are given without one
[MaxLength(MaxRules, ErrorMessageResourceName = "MaxLengthArray", ErrorMessageResourceType = typeof(ErrorsResource))]
public List<ServerRuleForm> Rules { get; set; } = [];
public ServerMediaLimitsForm Media { get; set; } = new();
public bool PublishActivity { get; set; } = false;
}
public class ServerRuleForm
{
[StringLength(64, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Id { get; set; }//an existing rule's id, or none for a new rule
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(500, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Text { get; set; }
[StringLength(1000, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Hint { get; set; }
}
// null keeps the configured value
public class ServerMediaLimitsForm
{
public const long HardMaxImageBytes = 40L * 1024 * 1024;
public const long HardMaxVideoBytes = 99L * 1024 * 1024;
public const int HardMaxSeconds = 4 * 3600;
[Range(0L, long.MaxValue, ErrorMessageResourceName = "Range", ErrorMessageResourceType = typeof(ErrorsResource))]
public long? QuotaBytesPerRoot { get; set; }//0: no quota
[Range(1L, HardMaxImageBytes, ErrorMessageResourceName = "Range", ErrorMessageResourceType = typeof(ErrorsResource))]
public long? MaxImageBytes { get; set; }
[Range(1L, HardMaxVideoBytes, ErrorMessageResourceName = "Range", ErrorMessageResourceType = typeof(ErrorsResource))]
public long? MaxVideoBytes { get; set; }
[Range(1, HardMaxSeconds, ErrorMessageResourceName = "Range", ErrorMessageResourceType = typeof(ErrorsResource))]
public int? MaxSeconds { get; set; }
}
// a relay the instance actor subscribes to: named by the configuration (Federation:Relays, removed only there) or by
// the settings (added and removed from the page)
public class ViewRelay
{
public string Address { get; set; }
public string Source { get; set; }//"configuration" or "settings"
public string State { get; set; }//"pending", "accepted", "rejected", or "none" before its first request
public string ActorURI { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? AnsweredAt { get; set; }
}
public class RelayForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(2048, ErrorMessageResourceName = "StringLength", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Address { get; set; }//the relay's actor (https://relay.example/actor) or its inbox
}
}