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
168 lines
8.0 KiB
C#
168 lines
8.0 KiB
C#
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
|
|
}
|
|
}
|