net10, the refactor finished, and federation that works
Build / Build (push) Successful in 28s
Deploy / privapub.thepra.dev (push) Successful in 19s

The tree had not compiled since its first commit: Group, DmGroup and
IGroupUsersService were referenced and never written, an IDE rename had
turned the user-settings DTO into the ViewAvatarServer enum, and the settings
were saved as an entity they no longer were.

Built now:
- Group (an ActivityPub Group actor with its own keys, members, invitation
  code and optional password) and DmGroup (a conversation), with
  /clientapi/group/{list,insert,update,join,leave,approve}.
- Posts and DMs: /clientapi/post/{list,insert,delete}, /clientapi/dm/{list,insert};
  DM recipients are local usernames or user@host handles resolved by WebFinger.
- Invitation sign-up and login against the group's invitation code, checking
  the password before any account is created.
- Federation: WebFinger, NodeInfo 2.0, actors at /peasants/{name} (Person,
  Group, and an Application instance actor) with SPKI keys, draft-cavage
  RSA-SHA256 HTTP signatures both ways, an inbox handling Follow (+Accept),
  Undo, Create, Delete and Update, an outbox, notes at /posts/{id}, and a
  persisted, retried, signed delivery queue. A post to a group is announced
  by the group to its followers (FEP-1b12).
- The unused, broken typed ActivityPub models are replaced by a renderer;
  NSign's HMAC setup, which could not federate, is gone.

Upgrade: net10.0, MongoDB.Entities 25.1 (instance DB API, Standard GUIDs),
Swashbuckle 10 / OpenApi 2, Serilog.AspNetCore 10, MailKit 4.18,
PasswordGenerator 3. The JWT keys are 64 bytes (IdentityModel 8 refuses
shorter for HS512). Production runs its own mongod on 127.0.0.1:27022, as
Sintopia's apps do, and deploys to privapub.thepra.dev from the build runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-01 08:05:56 +02:00
1 parent 56c5396106
commit 075c22228a
78 files changed
+3419 -1091

No files matched your search

@@ -1,44 +0,0 @@
using PrivaPub.Models.ActivityPub.Extra;
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubActivity))]
public class ActivityPubActivity : ActivityPubObject
{
[JsonPropertyName("actor")]
public ActivityPubActor Actor { get; set; }
[JsonPropertyName("object")]
public ActivityPubObject Object { get; set; }
[JsonPropertyName("target")]
public ActivityPubObject Target { get; set; }
[JsonPropertyName("result")]
public ActivityPubResult Result { get; set; }
[JsonPropertyName("origin")]
public ActivityPubOrigin Origin { get; set; }
[JsonPropertyName("instrument")]
public ActivityPubInstrument Instrument { get; set; }
[JsonIgnore]
public bool NeedsObjectForInbox => Type is
ObjectType.Create or
ObjectType.Update or
ObjectType.Delete or
ObjectType.Follow or
ObjectType.Add or
ObjectType.Remove or
ObjectType.Like or
ObjectType.Block or
ObjectType.Undo;
[JsonIgnore]
public bool NeedsTargetForInbox => Type is
ObjectType.Add or
ObjectType.Remove;
}
}
@@ -1,79 +0,0 @@
using PrivaPub.Models.ActivityPub.Extra;
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubActor))]
public class ActivityPubActor : ActivityPubObject
{
[JsonPropertyName("type")]
public new ObjectType? Type => ObjectType.Person;
[JsonPropertyName("inbox")]
public string Inbox { get; set; }
[JsonPropertyName("outbox")]
public string Outbox { get; set; }
[JsonPropertyName("url")]
public string ProfileURL { get; set; }
[JsonPropertyName("preferredUsername")]
public string PreferredUsername { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("icon")]
public ActivityPubIcon Icon { get; set; }
[JsonPropertyName("image")]
public ActivityPubIcon Thumbnail { get; set; }
[JsonPropertyName("manuallyApprovesFollowers")]
public bool ManuallyApprovesFollowers { get; set; } = false;
[JsonPropertyName("discoverable")]
public bool Discoverable { get; set; } = true;
[JsonPropertyName("publicKey")]
public ActivityPubPublicKey PublicKey { get; set; } = new();
[JsonPropertyName("endpoints")]
public ActivityPubActorEndpoints Endpoints { get; set; } = new();
[JsonPropertyName("attachment")]
public IEnumerable<ActivityPubAttachment> Attachment { get; set; } = Enumerable.Empty<ActivityPubAttachment>();
[JsonPropertyName("tag")]
public IEnumerable<string> Tags => Enumerable.Empty<string>();
}
public class ActivityPubAttachment
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("value")]
public string Value { get; set; }
}
public class ActivityPubActorEndpoints
{
[JsonPropertyName("sharedInbox")]
public string SharedInboxURL { get; set; }
[JsonPropertyName("oauthAuthorizationEndpoint")]
public string OAuthAuthorizationEndpoint { get; set; }
[JsonExtensionData]
public Dictionary<string, object> OtherData { get; set; }
}
public class ActivityPubPublicKey
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("owner")]
public string Owner { get; set; }
[JsonPropertyName("publicKeyPem")]
public string PublicKeyPem { get; set; }
}
}
@@ -1,24 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubCollection))]
public class ActivityPubCollection : ActivityPubObject
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.Collection;
[JsonPropertyName("current")]
public string RecentlyUpdatedItem { get; set; }
[JsonPropertyName("first")]
public string FirstItem { get; set; }
[JsonPropertyName("last")]
public string LastItem { get; set; }
[JsonPropertyName("items")]
public List<ActivityPubLink> Items { get; set; }
[JsonPropertyName("totalItems")]
public int TotalItems => Items.Count;
}
}
@@ -1,18 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubCollectionPage))]
public class ActivityPubCollectionPage : ActivityPubCollection
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.CollectionPage;
[JsonPropertyName("partOf")]
public string BaseCollectionLink { get; set; }
[JsonPropertyName("next")]
public string NextCollectionLink { get; set; }
[JsonPropertyName("prev")]
public string PreviousCollectionLink { get; set; }
}
}
@@ -1,28 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubLink))]
public class ActivityPubLink : ActivityPubObject
{
[JsonPropertyName("id")]
public new ObjectType Type { get; set; } = ObjectType.Mention;
[JsonPropertyName("href")]
public string Href { get; set; }
[JsonPropertyName("preview")]
public string Preview { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("rel")]
public string Relation { get; set; }
[JsonPropertyName("hreflang")]
public string HrefLang { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
}
}
@@ -1,126 +0,0 @@
using PrivaPub.Models.ActivityPub.Extra;
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubObject))]
public partial class ActivityPubObject
{
[JsonPropertyName("@context")]
public List<object> Context => new()
{
"https://www.w3.org/ns/activitystreams",
"https://{0}/schemas/litepub-0.1.jsonld",
new ActivityPubContextLanguage()
};
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
public ObjectType? Type { get; set; }
[JsonPropertyName("mediaType")]
public string MediaType { get; set; }
[JsonPropertyName("published")]
public DateTime? Published { get; set; }
[JsonPropertyName("updated")]
public DateTime? Updated { get; set; }
[JsonExtensionData]
public Dictionary<string, object> OtherData { get; set; }
[JsonPropertyName("source")]
public ActivityPubSource Source { get; set; }
//and part of link
[JsonPropertyName("to")]
public string[] To { get; set; }
[JsonPropertyName("cc")]
public string[] Cc { get; set; }
[JsonPropertyName("bcc")]
public string[] Bcc { get; set; }
[JsonPropertyName("bto")]
public string[] Bto { get; set; }
[JsonPropertyName("audience")]
public ActivityPubAudience Audience { get; set; }
[JsonIgnore]
public MacroType MacroType => Type switch
{
ObjectType.Article or
ObjectType.Audio or
ObjectType.Document or
ObjectType.Event or
ObjectType.Image or
ObjectType.Note or
ObjectType.Page or
ObjectType.Place or
ObjectType.Profile or
ObjectType.Relationship or
ObjectType.Tombstone or
ObjectType.Video => MacroType.Object,
ObjectType.Mention => MacroType.Link,
ObjectType.Application or
ObjectType.Group or
ObjectType.Organization or
ObjectType.Person or
ObjectType.Service => MacroType.Actor,
ObjectType.Accept or
ObjectType.Add or
ObjectType.Announce or
ObjectType.Arrive or
ObjectType.Block or
ObjectType.Create or
ObjectType.Delete or
ObjectType.Dislike or
ObjectType.Flag or
ObjectType.Follow or
ObjectType.Ignore or
ObjectType.Invite or
ObjectType.Join or
ObjectType.Leave or
ObjectType.Like or
ObjectType.Listen or
ObjectType.Move or
ObjectType.Offer or
ObjectType.Question or
ObjectType.Reject or
ObjectType.Read or
ObjectType.Remove or
ObjectType.TentativeReject or
ObjectType.TentativeAccept or
ObjectType.Travel or
ObjectType.Undo or
ObjectType.Update or
ObjectType.View => MacroType.Activity,
ObjectType.Collection => MacroType.Collection,
ObjectType.CollectionPage => MacroType.CollectionPage,
ObjectType.OrderedCollection => MacroType.OrderedCollection,
ObjectType.OrderedCollectionPage => MacroType.OrderedCollectionPage,
_ => MacroType.Unknown
};
}
public class ActivityPubContextLanguage
{
[JsonPropertyName("@language")]
public string Language { get; set; } = "und";
}
public class ActivityPubSource
{
[JsonPropertyName("content")]
public string Content { get; set; }
[JsonPropertyName("mediaType")]
public string MediaType { get; set; }
}
}
@@ -1,17 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubOrderedCollection))]
public class ActivityPubOrderedCollection : ActivityPubCollection
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.OrderedCollection;
[JsonPropertyName("orderedItems")]
public List<ActivityPubLink> OrderedItems { get; set; }
[JsonPropertyName("totalItems")]
public new int TotalItems => OrderedItems?.Count ?? default;
}
}
@@ -1,27 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubOrderedCollectionPage))]
public class ActivityPubOrderedCollectionPage : ActivityPubCollection
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.OrderedCollectionPage;
[JsonPropertyName("partOf")]
public string BaseCollectionLink { get; set; }
[JsonPropertyName("next")]
public string NextCollectionLink { get; set; }
[JsonPropertyName("prev")]
public string PreviousCollectionLink { get; set; }
[JsonPropertyName("startIndex")]
public uint? StartIndex { get; set; }
[JsonPropertyName("orderedItems")]
public List<ActivityPubLink> OrderedItems { get; set; }
[JsonPropertyName("totalItems")]
public new int TotalItems => OrderedItems?.Count ?? default;
}
}
@@ -1,14 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubTombstone))]
public class ActivityPubTombstone : ActivityPubObject
{
[JsonPropertyName("formerType")]
public ObjectType FormerType { get; set; } = ObjectType.Unknown;
[JsonPropertyName("deleted")]
public DateTime Deleted { get; set; } = DateTime.UtcNow;
}
}
@@ -1,13 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubAudience
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}
@@ -1,19 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubIcon
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("url")]
public string URL { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
}
}
@@ -1,13 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubInstrument
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}
@@ -1,13 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubOrigin
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}
@@ -1,13 +0,0 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubResult
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}
-17
View File
@@ -1,17 +0,0 @@
namespace PrivaPub.Models.ActivityPub
{
public enum MacroType
{
Object,
Link,
Actor,
Activity,
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
Unknown
}
}
-63
View File
@@ -1,63 +0,0 @@
namespace PrivaPub.Models.ActivityPub
{
public enum ObjectType
{
//Object types
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
//Link types
Mention,
//Actor
Application,
Group,
Organization,
Person,
Service,
//Activity
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
//Collections
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
//NULL or default
Unknown
}
}
+2 -1
View File
@@ -1,4 +1,4 @@
using MongoDB.Entities;
using MongoDB.Entities;
using PrivaPub.Models.Email;
using PrivaPub.StaticServices;
@@ -14,6 +14,7 @@ namespace PrivaPub.Models
public string ValidDiscussionFilesTypes { get; set; }
public List<string> SupportedLanguages { get; set; } = new();
public string BackendBaseAddress { get; set; }
public string FrontendBaseAddress { get; set; }
public EmailConfiguration EmailConfiguration { get; set; }
public HashingOptions HashingOptions { get; set; }
public DateTime LastUpdateDate { get; set; } = DateTime.UtcNow;
+18
View File
@@ -0,0 +1,18 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Federation
{
public class Delivery : Entity
{
public string SignerId { get; set; }
public LocalActorKind SignerKind { get; set; }
public string InboxURL { get; set; }
public string Body { get; set; }
public int Attempts { get; set; }
public DateTime NextAttemptAt { get; set; } = DateTime.UtcNow;
public DateTime? DeliveredAt { get; set; }
public DateTime? AbandonedAt { get; set; }
public string LastError { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
}
}
+23
View File
@@ -0,0 +1,23 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Federation
{
public class Follower : Entity
{
public string LocalActorId { get; set; }
public LocalActorKind LocalActorKind { get; set; }
public string ActorURI { get; set; }
public string InboxURL { get; set; }
public string SharedInboxURL { get; set; }
public string FollowActivityURI { get; set; }
public bool IsAccepted { get; set; } = true;
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
}
public enum LocalActorKind
{
Person,
Group,
Application
}
}
@@ -0,0 +1,11 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Federation
{
public class InstanceActor : Entity
{
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
}
}
+15
View File
@@ -0,0 +1,15 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Group
{
public class DmGroup : Entity
{
public string Description { get; set; }
public List<GroupMember> Members { get; set; } = new();
public string ConversationURI { get; set; }//context of the federated thread
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletionAt { get; set; }
}
}
+47
View File
@@ -0,0 +1,47 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Group
{
public class Group : Entity
{
public string UserName { get; set; }//preferredUsername
public string Name { get; set; }//name
public string Description { get; set; }//summary, Markdown
public string Url { get; set; }
public string Domain { get; set; }
public string InboxURL { get; set; }
public string OutboxURL { get; set; }
public string PictureURL { get; set; }//icon
public string ThumbnailURL { get; set; }//image
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public bool IsDiscoverable { get; set; } = true;
public bool ManuallyApprovesMembers { get; set; }
public string OwnerAvatarId { get; set; }
public List<GroupMember> Members { get; set; } = new();
public string InvitationCode { get; set; }
public string HashedInvitationPassword { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletionAt { get; set; }
}
public class GroupMember
{
public string AvatarId { get; set; }//local Avatar.ID, or the actor URI when IsForeign
public bool IsForeign { get; set; }
public GroupRole Role { get; set; } = GroupRole.Member;
public DateTime JoinedAt { get; set; } = DateTime.UtcNow;
}
public enum GroupRole
{
Member,
Moderator,
Owner
}
}
+3 -1
View File
@@ -1,4 +1,4 @@
using MongoDB.Entities;
using MongoDB.Entities;
namespace PrivaPub.Models.Post
{
@@ -14,6 +14,8 @@ namespace PrivaPub.Models.Post
public bool HasContentWarning { get; set; } = false;
public bool IsFederatedCopy { get; set; }
public string ObjectURI { get; set; }//the Note's id
public string ActorURI { get; set; }//attributedTo
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? UpdateDate { get; set; } = DateTime.UtcNow;
+3 -1
View File
@@ -1,4 +1,4 @@
using MongoDB.Entities;
using MongoDB.Entities;
namespace PrivaPub.Models.Post
{
@@ -15,6 +15,8 @@ namespace PrivaPub.Models.Post
public bool HasContentWarning { get; set; } = false;
public bool IsFederatedCopy { get; set; }
public string ObjectURI { get; set; }//the Note's id
public string ActorURI { get; set; }//attributedTo
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? UpdateDate { get; set; } = DateTime.UtcNow;
+4 -1
View File
@@ -1,4 +1,4 @@
using MongoDB.Entities;
using MongoDB.Entities;
namespace PrivaPub.Models.User
{
@@ -55,6 +55,9 @@ namespace PrivaPub.Models.User
public string ModerationNote { get; set; }
public bool IsDiscoverable { get; set; } = true;
public string ActorURI { get; set; }//id
public string PublicKeyId { get; set; }
public string SharedInboxURL { get; set; }
public string InboxURL { get; set; }
public string OutboxURL { get; set; }
public string MovedToURL { get; set; }
-24
View File
@@ -1,24 +0,0 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Entities;
namespace PrivaPub.Models.Data
{
[BsonIgnoreExtraElements]
public class PrivaPub : Entity
{
public DateTime Timestamp { get; set; }
public string Level { get; set; }
public string RenderedMessage { get; set; }
[BsonIgnoreIfDefault, BsonIgnoreIfNull]
public DbLogProperties Properties { get; set; }
[BsonIgnoreIfDefault, BsonIgnoreIfNull]
public string Exception { get; set; }
}
[BsonIgnoreExtraElements]
public class DbLogProperties
{
public string SourceContext { get; set; }
}
}