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

@@ -0,0 +1,30 @@
using PrivaPub.ClientModels.Resources;
using PrivaPub.ClientModels.ValidatorAttributes;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Group
{
public class InsertGroupForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(32, MinimumLength = 3, ErrorMessageResourceName = "StringLengthMinMax", ErrorMessageResourceType = typeof(ErrorsResource)),
RegularExpression("^[a-z0-9_]+$", ErrorMessageResourceName = "EmptySpacesNotAllowed", ErrorMessageResourceType = typeof(ErrorsResource)),
NoWhiteSpaces]
public string UserName { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(64, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Name { get; set; }
[StringLength(2000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Description { get; set; }
public string InvitationPassword { get; set; }
public bool IsDiscoverable { get; set; } = true;
public bool ManuallyApprovesMembers { get; set; }
}
}
@@ -0,0 +1,28 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Group
{
public class JoinGroupForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(Constants.GroupInvitationLength, MinimumLength = Constants.GroupInvitationLength,
ErrorMessageResourceName = "StringLengthMinMax", ErrorMessageResourceType = typeof(ErrorsResource))]
public string InvitationCode { get; set; }
public string InvitationPassword { get; set; }
}
public class GroupMembershipForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string GroupId { get; set; }
public string MemberActorURI { get; set; }
}
}
@@ -0,0 +1,25 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Group
{
public class UpdateGroupForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string GroupId { get; set; }
[StringLength(64, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Name { get; set; }
[StringLength(2000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Description { get; set; }
public string InvitationPassword { get; set; }
public bool RemoveInvitationPassword { get; set; }
public bool RegenerateInvitationCode { get; set; }
public bool? IsDiscoverable { get; set; }
public bool? ManuallyApprovesMembers { get; set; }
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace PrivaPub.ClientModels.Group
{
public class ViewGroup
{
public string Id { get; set; }
public string UserName { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public string Handle { get; set; }
public bool IsDiscoverable { get; set; }
public bool ManuallyApprovesMembers { get; set; }
public bool IsOwner { get; set; }
public string InvitationCode { get; set; }
public bool IsPasswordRequired { get; set; }
public int MembersCount { get; set; }
public DateTime CreationDate { get; set; }
}
}
@@ -0,0 +1,14 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels
{
public class InvitationLoginForm : LoginForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(Constants.GroupInvitationLength, MinimumLength = Constants.GroupInvitationLength,
ErrorMessageResourceName = "StringLengthMinMax", ErrorMessageResourceType = typeof(ErrorsResource))]
public string InvitationCode { get; set; }
}
}
@@ -0,0 +1,45 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Post
{
public class InsertPostForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(5000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Text { get; set; }
[StringLength(200, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Title { get; set; }
public string GroupId { get; set; }
public string AnsweringToPostId { get; set; }
public bool HasContentWarning { get; set; }
}
public class InsertDmForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(5000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Text { get; set; }
public string DmGroupId { get; set; }
public List<string> Recipients { get; set; } = new();//local usernames or user@host handles
public bool HasContentWarning { get; set; }
}
public class DeletePostForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AvatarId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource))]
public string PostId { get; set; }
}
}
+25
View File
@@ -0,0 +1,25 @@
namespace PrivaPub.ClientModels.Post
{
public class ViewPost
{
public string Id { get; set; }
public string ObjectURI { get; set; }
public string AuthorAvatarId { get; set; }
public string AuthorActorURI { get; set; }
public string GroupId { get; set; }
public string DmGroupId { get; set; }
public string AnsweringToPostId { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public bool HasContentWarning { get; set; }
public bool IsFederatedCopy { get; set; }
public DateTime CreationDate { get; set; }
}
public class ViewDmGroup
{
public string Id { get; set; }
public List<string> Members { get; set; } = new();
public DateTime UpdatedAt { get; set; }
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<!--<Nullable>enable</Nullable>-->
</PropertyGroup>
@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
@@ -396,6 +396,30 @@ namespace PrivaPub.ClientModels.Resources {
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string AvatarId {
get {
return ResourceManager.GetString("AvatarId", resourceCulture);
}
}
public static string Biography {
get {
return ResourceManager.GetString("Biography", resourceCulture);
}
}
public static string LanguageCode {
get {
return ResourceManager.GetString("LanguageCode", resourceCulture);
}
}
public static string UserName {
get {
return ResourceManager.GetString("UserName", resourceCulture);
}
}
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -315,4 +315,16 @@
<data name="Usernames to search and add" xml:space="preserve">
<value>Soprannomi da ricercare e aggiungere</value>
</data>
</root>
<data name="AvatarId" xml:space="preserve">
<value>Avatar</value>
</data>
<data name="Biography" xml:space="preserve">
<value>Biografia</value>
</data>
<data name="LanguageCode" xml:space="preserve">
<value>Lingua</value>
</data>
<data name="UserName" xml:space="preserve">
<value>Nome utente</value>
</data>
</root>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -315,4 +315,16 @@
<data name="Usernames to search and add" xml:space="preserve">
<value>Usernames to search and add</value>
</data>
</root>
<data name="AvatarId" xml:space="preserve">
<value>Avatar</value>
</data>
<data name="Biography" xml:space="preserve">
<value>Biography</value>
</data>
<data name="LanguageCode" xml:space="preserve">
<value>Language</value>
</data>
<data name="UserName" xml:space="preserve">
<value>Username</value>
</data>
</root>
@@ -1,10 +1,12 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace PrivaPub.ClientModels.User.Avatar
{
[JsonSerializable(typeof(ViewAvatar))]
public class ViewAvatar
{
public string Id { get; set; }
public string Handle { get; set; }
public string Url { get; set; }//url
public string Name { get; set; }//name
public string UserName { get; set; }//preferredUsername
+2 -2
View File
@@ -1,4 +1,4 @@
namespace PrivaPub.ClientModels.User
namespace PrivaPub.ClientModels.User
{
public class JwtUser
{
@@ -8,6 +8,6 @@
public List<string> Policies { get; set; } = new();
public string Token { get; set; }
public long Expiration { get; set; }
public ViewAvatarServer UserSettings { get; set; } = new();
public ViewUserSettings UserSettings { get; set; } = new();
}
}
@@ -0,0 +1,25 @@
using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.User
{
public class ViewUserSettings
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = nameof(LanguageCode), ResourceType = typeof(FieldsNameResource))]
public string LanguageCode { get; set; } = "en";
[Range(0, 359, ErrorMessageResourceName = nameof(Range), ErrorMessageResourceType = typeof(ErrorsResource))]
public short LightThemeIndexColour { get; set; } = 25;
[Range(0, 359, ErrorMessageResourceName = nameof(Range), ErrorMessageResourceType = typeof(ErrorsResource))]
public short DarkThemeIndexColour { get; set; } = 215;
[Range(-2, 359, ErrorMessageResourceName = nameof(Range), ErrorMessageResourceType = typeof(ErrorsResource))]
public short IconsThemeIndexColour { get; set; } = 25;
public bool PreferSystemTheming { get; set; } = false;
public bool ThemeIsDarkMode { get; set; } = false;
public bool ThemeIsLightGray { get; set; } = false;
public bool ThemeIsDarkGray { get; set; } = false;
}
}