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 @@ - +