Skip to content

IsaacSchemm/Crowmask

Repository files navigation

Crowmask 🐦‍⬛🎭

Crowmask is a combination ActivityPub server and Bluesky bot, written in C# and F#, that runs on Azure Functions and Cosmos DB and mirrors artwork (and optionally journal entries) from a Weasyl account.

Crowmask implements ActivityPub (server-to-server), exposing a user account with the name, profile, and avatar of the attached Weasyl user (the user associated with the Weasyl API key configured in the app's settings). Mastodon and Pixelfed users can follow this account.

Crowmask can also connect to a Bluesky account, creating and deleting artwork posts as needed (all posts are backdated, and posts for edited submissions are deleted and re-created).

When a user likes, replies to, or shares/boosts one of this account's posts, or tags the Crowmask actor in a post, Crowmask will send a private Note to any ActivityPub accounts that are configured as "admin actors" (typically, there would be a single account configured this way). Bluesky notifications are checked every six hours and summarized in a private Note sent to these admin actors.

Browsing

Crowmask is primarily an ActivityPub server and bot, but it does include profile and submission pages. As long as ReturnHTML is set to true in Program.cs, you can access these pages by pointing a web browser at any of the ActivityPub URLs.

Certain parameters set in Program.cs affect content negotiation. By default, Crowmask will return Markdown renditions of its pages to any user agent that does not specify ActivityPub JSON or HTML in its Accept header, and it will redirect web browsers that try to access the URLs for individual posts to the corresponding Weasyl pages (but it will not do the same with the actor URL).

Crowmask implements ActivityPub, HTML, and Markdown responses through content negotiation. The RSS and Atom feeds are implemented on the endpoint for page 1 of the outbox, but must be explicitly requested with format=rss or format=atom.

Implementation details

Layers

  • Crowmask.ATProto: a small Bluesky API client. Only implements functionality needed for Crowmask.
  • Crowmask.Interfaces: contains interfaces used to pass config values between layers or to allow inner layers to call outer-layer code.
  • Crowmask.Data: contains the data types and and data context, which map to documents in the Cosmos DB backend of EF Core.
  • Crowmask.LowLevel:
    • determines when cached posts are considered stale;
    • converts data objects like Submission (which are specific to the database schema) to more general F# records, then to ActivityPub objects or Markdown / HTML pages;
    • maps Crowmask internal IDs to ActivityPub IDs;
    • talks to the Weasyl API;
    • and performs content negotiation.
  • Crowmask.HighLevel:
    • ATProto: Creates, updates, and deletes Bluesky posts.
    • Signatures: HTTP signature validation.
    • Remote: Talks to other ActivityPub servers.
    • FeedBuilder: Implements RSS and Atom feeds.
    • RemoteInboxLocator: Collects inbox URLs for the admin actors, followers, and other known servers.
    • SubmissionCache: Retrieves and updates submissions in Crowmask's database.
    • UserCache: Retrieves and updates the user profile in Crowmask's database.
  • Crowmask: The main Azure Functions project, responsible for handling HTTP requests and running timed functions.

Public HTTP endpoints

  • /.well-known/nodeinfo: returns the location of the NodeInfo endpoint
  • /.well-known/webfinger: returns information about the actor, if given the actor's URL or a handle representing the actor on either CrowmaskHost or HandleHost; otherwise, redirects to the same path on the domain of the first admin actor
  • /api/actor: returns the Person object
  • /api/actor/followers: contains the IDs of all followers (not paginated)
  • /api/actor/following: an empty list
  • /api/actor/inbox: processes incoming activities
  • /api/actor/nodeinfo: returns a NodeInfo 2.2 response
  • /api/actor/outbox: provides the number of submissions and a link to the first outbox page
  • /api/actor/outbox/page: contains Create activities for known cached Weasyl posts, newest first; also handles Atom and RSS (20 per page)
  • /api/journals/{journalid}: returns the resulting ActivityPub object
  • /api/submissions/{submitid}: returns the resulting ActivityPub object

Private HTTP endpoints

These functions must be authenticated using the X-Weasyl-API-Key header, and the value of the header must be the same as the Weasyl API key that Crowmask is configured to use.

  • POST /api/journals/{journalid}/refresh: triggers a refresh of the journal entry
  • POST /api/submissions/{submitid}/refresh: triggers a refresh of the submission
    • Optional query string parameter alt=: sets the alt text of the image

Timed refresh functions

  • RefreshRecent (every five minutes)
    • Cached posts with an upstream post date within the last hour (posts this recent are always considered stale)
  • RefreshStale (every day at 12:00)
    • All stale cached posts
  • RefreshUpstreamFull (every month on the 5th at 17:00)
    • All submissions in the user's Weasyl gallery
  • RefreshUpstreamNew (every day at 16:15)
    • All submissions in the user's Weasyl gallery that are newer than the most recent cached post

Other timed funtions

  • CheckBlueskyNotifications (every six hours at :00)
  • RefreshProfile (every day at 16:00)
  • SendOutbound (every hour at :02)

Additional information

Crowmask stands for "Content Read Off Weasyl: Modified ActivityPub Starter Kit". It began as an attempt to port ActivityPub Starter Kit to .NET, but was quickly modified to support the strongly typed nature of .NET and the specificity of this app. Still, having a simple, working ActivityPub implementation in a language I was able to understand (if not compile) was incredibly helpful, as was running a microblog.pub instance and being able to see the logs.

HTTP signature validation is adapted from Letterbook.

Example local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AdminActor": "https://pixelfed.example.com/users/...",
    "CosmosDBAccountEndpoint": "https://example.documents.azure.com:443/",
    "CosmosDBAccountKey": "...",
    "CrowmaskHost": "crowmask.example.com",
    "HandleHost": "crowmask.example.com",
    "HandleName": "activitypub-username-here",
    "KeyVaultHost": "crowmask.vault.azure.net",
    "WeasylApiKey": "...",
    "ATProtoPDS": "conocybe.us-west.host.bsky.network",
    "ATProtoHandle": "example.bsky.social",
    "ATProtoIdentifier": "username@example.com",
    "ATProtoPassword": "xxxxxxxxxxxxxx",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

Settings prefixed with ATProto can be omitted if you don't need the Bluesky bot. Crowmask stores Bluesky tokens in its database, so ATProtoIdentifier and ATProtoPassword should be removed once tokens are established.

You should be able to leave AdminActor blank or omit it to run the server without an admin actor (and without the ability to see who's interacted with your posts), although this hasn't been tested.

For Key Vault, the app is set up to use Managed Identity - turn this on in the Function App (Settings > Identity) then go to the key vault's access control (IAM) tab to give a role assignment of Key Vault Crypto User to that new managed identity.

For Cosmos DB, you will need to create the container in Data Explorer:

  • Database ID: Crowmask
  • Container ID: CrowmaskDbContext
  • Partition key: __partitionKey

If you don't want to include CosmosDBAccountKey, then Crowmask will try to use role-based access control (RBAC) via DefaultAzureCredential. In that case, run something like this in Azure CLI (PowerShell) to give the appropriate permissions to the function app's managed identity:

az login
az cosmosdb sql role assignment create --account-name {...} --resource-group {...} --scope "/" --principal-id {...} --role-definition-id 00000000-0000-0000-0000-000000000002

The {...} for the principal ID should be the ID shown in the Identity tab of the function app settings.

About

A single-actor ActivityPub server that mirrors artwork from a Weasyl account

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published