diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
new file mode 100644
index 0000000..fbbb448
--- /dev/null
+++ b/.gitea/workflows/build.yml
@@ -0,0 +1,16 @@
+name: Build
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+
+jobs:
+ build:
+ name: Build
+ runs-on: build
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build
+ run: dotnet build PrivaPub.sln -c Release
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..de22703
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,88 @@
+name: Deploy
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - 'v*'
+
+env:
+ UNIT: privapub
+ WEB_ROOT: /var/www/privapub.thepra.dev
+ BACKUPS: /var/backups/privapub.thepra.dev
+ LOCAL_URL: http://127.0.0.1:6970
+ PUBLIC_URL: https://privapub.thepra.dev
+
+jobs:
+ site:
+ name: privapub.thepra.dev
+ runs-on: build
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Resolve the build identity
+ run: |
+ echo "BUILD_COMMIT=$(echo "$GITHUB_SHA" | cut -c1-8)" >> "$GITHUB_ENV"
+ echo "BUILD_REF=${GITHUB_REF_NAME:-master}" >> "$GITHUB_ENV"
+ echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"
+
+ - name: Publish
+ run: |
+ rm -rf "$GITHUB_WORKSPACE/publish"
+ dotnet publish PrivaPub/PrivaPub.csproj -c Release -r linux-x64 --self-contained true \
+ -o "$GITHUB_WORKSPACE/publish" \
+ -p:BuildCommit="$BUILD_COMMIT" -p:BuildRef="$BUILD_REF" -p:BuildTimeUtc="$BUILD_TIME"
+
+ - name: Assert the publish actually produced a build
+ run: |
+ P="$GITHUB_WORKSPACE/publish"
+ for f in PrivaPub PrivaPub.dll PrivaPub.ClientModels.dll appsettings.Production.json Data/languagesNative.json; do
+ [ -e "$P/$f" ] || { echo "::error::publish output is missing $f"; exit 1; }
+ done
+ grep -q '"includedFrameworks"' "$P/PrivaPub.runtimeconfig.json" \
+ || { echo "::error::publish is not self-contained"; exit 1; }
+ age=$(( $(date +%s) - $(stat -c %Y "$P/PrivaPub.dll") ))
+ [ "$age" -lt 3600 ] || { echo "::error::PrivaPub.dll is ${age}s old - the publish reused a stale artifact"; exit 1; }
+ echo "publish OK, $(du -sh "$P" | cut -f1)"
+
+ - name: Snapshot the live directory
+ run: |
+ STAMP=$(date +%Y%m%d-%H%M%S)
+ rsync -a "$WEB_ROOT/" "$BACKUPS/site-$STAMP/"
+ echo "SNAPSHOT=$BACKUPS/site-$STAMP" >> "$GITHUB_ENV"
+ ls -1dt "$BACKUPS"/site-* 2>/dev/null | tail -n +4 | xargs -r rm -rf || true
+
+ - name: Stop, sync, start
+ run: |
+ sudo systemctl stop "$UNIT"
+ rsync -a --delete --exclude 'appsettings.Development.json' "$GITHUB_WORKSPACE/publish/" "$WEB_ROOT/"
+ chgrp www-data "$WEB_ROOT/appsettings.Production.json"
+ chmod 640 "$WEB_ROOT/appsettings.Production.json"
+ chmod +x "$WEB_ROOT/PrivaPub"
+ sudo systemctl start "$UNIT"
+
+ - name: Health check, roll back on failure
+ run: |
+ ok=0
+ for i in $(seq 1 40); do
+ code=$(curl -s -o /dev/null -w '%{http_code}' "$LOCAL_URL/build.json" || true)
+ if [ "$code" = "200" ]; then ok=1; break; fi
+ sleep 3
+ done
+ if [ "$ok" != "1" ]; then
+ echo "::error::the site did not come back healthy - rolling back to $SNAPSHOT"
+ sudo systemctl stop "$UNIT"
+ rsync -a --delete "$SNAPSHOT/" "$WEB_ROOT/"
+ sudo systemctl start "$UNIT" || true
+ exit 1
+ fi
+
+ - name: Verify what is being served
+ run: |
+ served=$(curl -fsS "$PUBLIC_URL/build.json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('commit',''))")
+ [ "$served" = "$BUILD_COMMIT" ] || { echo "::error::served build is '$served', expected '$BUILD_COMMIT'"; exit 1; }
+ code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$PUBLIC_URL/peasants/privapub")
+ [ "$code" = "200" ] || { echo "::error::the instance actor answered $code"; exit 1; }
+ code=$(curl -s -o /dev/null -w '%{http_code}' "$PUBLIC_URL/.well-known/nodeinfo")
+ [ "$code" = "200" ] || { echo "::error::nodeinfo answered $code"; exit 1; }
+ echo "::notice::serving $served"
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..8f315ce
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+ local
+ local
+
+
+
+
diff --git a/PrivaPub.ClientModels/Group/InsertGroupForm.cs b/PrivaPub.ClientModels/Group/InsertGroupForm.cs
new file mode 100644
index 0000000..217e6fc
--- /dev/null
+++ b/PrivaPub.ClientModels/Group/InsertGroupForm.cs
@@ -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; }
+ }
+}
diff --git a/PrivaPub.ClientModels/Group/JoinGroupForm.cs b/PrivaPub.ClientModels/Group/JoinGroupForm.cs
new file mode 100644
index 0000000..ca3baf9
--- /dev/null
+++ b/PrivaPub.ClientModels/Group/JoinGroupForm.cs
@@ -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; }
+ }
+}
diff --git a/PrivaPub.ClientModels/Group/UpdateGroupForm.cs b/PrivaPub.ClientModels/Group/UpdateGroupForm.cs
new file mode 100644
index 0000000..cf3449a
--- /dev/null
+++ b/PrivaPub.ClientModels/Group/UpdateGroupForm.cs
@@ -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; }
+ }
+}
diff --git a/PrivaPub.ClientModels/Group/ViewGroup.cs b/PrivaPub.ClientModels/Group/ViewGroup.cs
new file mode 100644
index 0000000..9a91fff
--- /dev/null
+++ b/PrivaPub.ClientModels/Group/ViewGroup.cs
@@ -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; }
+ }
+}
diff --git a/PrivaPub.ClientModels/InvitationLoginForm.cs b/PrivaPub.ClientModels/InvitationLoginForm.cs
new file mode 100644
index 0000000..b1da56c
--- /dev/null
+++ b/PrivaPub.ClientModels/InvitationLoginForm.cs
@@ -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; }
+ }
+}
diff --git a/PrivaPub.ClientModels/Post/InsertPostForm.cs b/PrivaPub.ClientModels/Post/InsertPostForm.cs
new file mode 100644
index 0000000..546566c
--- /dev/null
+++ b/PrivaPub.ClientModels/Post/InsertPostForm.cs
@@ -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 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; }
+ }
+}
diff --git a/PrivaPub.ClientModels/Post/ViewPost.cs b/PrivaPub.ClientModels/Post/ViewPost.cs
new file mode 100644
index 0000000..66890b2
--- /dev/null
+++ b/PrivaPub.ClientModels/Post/ViewPost.cs
@@ -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 Members { get; set; } = new();
+ public DateTime UpdatedAt { get; set; }
+ }
+}
diff --git a/PrivaPub.ClientModels/PrivaPub.ClientModels.csproj b/PrivaPub.ClientModels/PrivaPub.ClientModels.csproj
index 53b2676..44c8ab4 100644
--- a/PrivaPub.ClientModels/PrivaPub.ClientModels.csproj
+++ b/PrivaPub.ClientModels/PrivaPub.ClientModels.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net10.0
enable
diff --git a/PrivaPub.ClientModels/Resources/FieldsNameResource.Designer.cs b/PrivaPub.ClientModels/Resources/FieldsNameResource.Designer.cs
index 69e6c54..b309584 100644
--- a/PrivaPub.ClientModels/Resources/FieldsNameResource.Designer.cs
+++ b/PrivaPub.ClientModels/Resources/FieldsNameResource.Designer.cs
@@ -1,4 +1,4 @@
-//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
//
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
@@ -396,6 +396,30 @@ namespace PrivaPub.ClientModels.Resources {
///
/// Looks up a localized string similar to Name.
///
+ 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);
diff --git a/PrivaPub.ClientModels/Resources/FieldsNameResource.it.resx b/PrivaPub.ClientModels/Resources/FieldsNameResource.it.resx
index 9415499..3f1bccc 100644
--- a/PrivaPub.ClientModels/Resources/FieldsNameResource.it.resx
+++ b/PrivaPub.ClientModels/Resources/FieldsNameResource.it.resx
@@ -1,4 +1,4 @@
-
+