activitypub

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 25, 2024 License: BSD-3-Clause Imports: 16 Imported by: 1

README

go-activitypub

An opionated (and incomplete) ActivityPub service implementation in Go.

Background

The "Holding Hands with the "Fediverse" – ActivityPub at SFO Museum" blog post (March, 2024) is a good place to start. It is a long, but thorough, discussion of why, what and how SFO Museum is thinking about ActivityPub in relation to its collection and digital initiatives.

Additional reading

Documentation

The documentation for this package (and in particular godoc) is incomplete reflecting the nature of our work to first understand the mechanics, and second explore the tolerances, of the ActivityPub protocols.

In advance of more comprehensive documentation we have set a GitHub “Discussions” group where people can ask questions or offer suggestions:

Motivation

This is on-going and exploratory work to develop a limited "social media" style ActivityPub service. Although it is not necessarily "stable" in the sense that the code base may still change, without any guarantee of backwards compatibility, it does work and is currently deployed (in an experimental fashion) in production.

I find the documentation for ActivityPub very confusing. I don't think I have any problem(s) with the underlying specification but I have not found any implementation guides that haven't left me feeling more confused than when I started. This includes the actual ActivityPub specifications published by the W3C which are no doubt thorough but, as someone with a finite of amount of competing time to devote to reading those specs, often feel counter-productive. Likewise, working implementations of the ActivityPub standards are often a confusing maze of abstractions that become necessary to do everything defined in the specs. There are some third-party guides, listed below, which are better than others but so far each one has felt incomplete in one way or another.

Importantly, the point is not that any of these things are "bad". They clearly aren't as evidenced by the many different working implementations of the ActivityPub standards in use today. The point is that the documentation, as it exists, hasn't been great for me. This repository is an attempt to understand all the moving pieces and their relationship to one another by working through the implementation of a simple ActivityPub service. It is incomplete by design and, if you are reading this, it's entirely possible that parts of it remain incorrect.

The goal is implement a basic web service and a set of command line tools which allow:

  • Individual accounts to be created.
  • The ability for one account to follow, or unfollow, one another.
  • The ability for one account to block, or unblock, another account.
  • The ability for one account to post a message and to have that message relayed to one or more other accounts.
  • The ability for one account to see all the messages that have been delivered to them by other accounts.
  • The ability for an account to receive "boosts" and record them.
  • The ability for messages to be processed, out of bounds, after receipts using a messaging queue.

That's it, at least for now. It does have support for ActivityPub account migration, editing posts or notifications of changes to posts.

How does ActivityPub work?

This section has been moved in to the docs/ACTIVITYPUB.md document.

The Code

This is now the second iteration of the code and a major refactoring of the first. Specifically:

  • The code has been updated to deliver arbitrary ActivityPub "activities" rather than just "posts" (which are the internal representation of ActvityPub "notes"). This is the first attempt at separating the generic ActivityPub mechanics from the specifics of a social media style web application. At some point those mechanics will get moved in to their own package and/or this package will be renamed.
  • Moving relevant code in to domain-specific sub-packages, most notably the database packages.
  • Improved separations of concerns between the different components in order to facilitate the ease of reasoning about what any piece of the codebase does. This work is not complete and is blocked on still needing to figure out how and where things should live while preventing Go import cycle conflicts.
Architecture

Here is a high-level boxes-and-arrows diagram of the core components of this package:

There are four main components:

  1. A database layer (which is anything implemeting the interfaces for the "databases" or "tables", discussed in its own documentation.)
  2. A queueing layer (which is anything that implements the "delivery queue" or "message processing" interfaces, discussed in its own documentation.)
  3. A cmd/deliver-activity application for delivering messages which can be run from the command line or as an AWS Lambda function
  4. A cmd/server application which implements a subset of the ActvityPub related resources. These are: A /.well-known/webfinger resource for retrieving account information; Individual account resource pages; Individual account "inbox" resources; Minimalistic "permalink" pages for individual posts.

Importantly, this package does not implement ActivityPub "outboxes" yet. It is assumed that individual posts are written directly to your "posts" database/table and then registered with the delivery queue explicitly in your custom code. That doesn't mean it will always be this way. It just means it's that way right now. Take a look at cmd/create-post and app/post/create for an example of how to post messages manually.

For example, imagine that:

  • The purple box is an AWS DynamoDB database (with 13 separate tables)
  • The blue boxes are AWS SQS queues
  • The green boxes are AWS Lambda functions. The cmd/server function will need to be configured so that it is reachable from the internet whether that means it is configured as a Lambda Function URL or "fronted" by an API Gateway instance; those details are left as an exercise to the reader.

However, this same setup could be configured and deployed where:

  • The purple box is a MySQL database (with 13 separate tables)
  • The blue boxes are plain-vanilla "pub-sub" style queues
  • The green boxes are long-running daemons on a plain-vanilla Linux server

The point is that the code tries to be agnostic about these details. As such the code tries to define abstract "interfaces" for these high-level concepts in order to allow for a multiplicity of concrete implementations. Currently those implementations are centered on local and synchronous processes and AWS services but the hope is that it will be easy (or at least straightforward) to write custom implementations as needed.

Activities and "Activities"

As of this writing the go-activitypub package exposes two Activity types. This is not ideal but for the time being it just is. One is a generic ActivityPub construct and the other is specific to this package and how it processes ActivityPub messages.

ap.Activity

This is a struct encapsulating an ActivityPub/ActivityStreams activity message. It is defined in the ap package.

activitypub.Activity

This is a struct which represents and internal representation of an ActivityPub Activity message with pointers to other relevant internal representations of things like account IDs rather than actor, URIs, etc. It is defined in the root activitypub package.

Notes, Posts and Messages

"Notes" are the messages sent by an external actor to an account hosted by the go-activitypub package. They are the body (content) of an ActivityPub "note".

"Posts" are the internal representations of messages sent by an account hosted by the go-activitypub web service. They are transformed in to ActivityPub "notes" before delivery.

"Messages" are pointers to "notes" and the accounts they are delivered to. Messages exists to deduplicate notes which may have been sent to multiple accounts hosted by the go-activitypub web service.

Deliveries

There are four principal "layers" involved in delivering an ActivityPub "activity":

  • The PostToInbox method which is what actually delivers the raw bytes of an activity to a remote host (inbox). It takes as its input the message body, a private key and an inbox URI. Importantly, it does not know anything about databases or accounts or anything of the other underlying infrastructure. It simply sends messages.

  • The SendActivity method for Account instances which takes as its input the activity to send and a destination "inbox" and then calls the PostToInbox message, appending that accounts private key.

  • The DeliverActivity method which takes as its input an ActivityPub activity, a @name@host address to deliver the activity and resolves sender actors to underlying accounts and to addresses to inboxes. Ultimately, it calls the account.SendActivity method and performs any additional logging steps.

  • The DeliverActivityToFollowers method takes as its input an ActivityPub activity and a delivery queue and resolves sender actors and schedules the message to be delivered to all of that actor's followers (using the delivery queue). The details of the delivery queue are unknown to this method but it is assumed that, eventually, the "other end" of the delivery queue will invoke the DeliverActivity method.

Databases

Documentation for databases has been moved in to database/README.md

Queues

Documentation for databases has been moved in to queue/README.md

Tools

Documentation for command line tools has been moved in to cmd/README.md

Example

This documentation has moved in to docs/EXAMPLE.md

Logging

This package uses the log/slog package for logging. The reality, though, is that between the inherent chatiness of ActivityPub (AP) to and from servers coupled with the endless abuse of bad actors probing and trying to perform mischief on AP endpoints the log output for a publicly accessible server can become "challenging". I am still thinking about how to deal with this.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("Not found")

ErrNotFound is an error indicating that an item is not present or was not found.

View Source
var ErrNotImplemented = errors.New("Not implemented")

ErrNotImplemented is an error indicating that a feature or specific functionality is not implemented.

Functions

func DefaultLimitedReader

func DefaultLimitedReader(r io.Reader) io.Reader

func NewLimitedReader

func NewLimitedReader(r io.Reader, n int64) io.Reader

Types

type Account

type Account struct {
	// Id is a unique numeric identifier for the account
	Id int64 `json:"id"`
	// AccountType denotes the ActivityPub account type
	AccountType AccountType `json:"account_type"`
	// Name is the unique account name for the account
	Name string `json:"name"`
	// DisplayName is the long-form name for the account (which is not guaranteed to be unique across all accounts)
	DisplayName string `json:"display_name"`
	// Blurb is the descriptive text for the account
	Blurb string `json:"blurb"`
	// URL is the primary website associated with the account
	URL string `json:"url"`
	// PublicKeyURI is a valid `gocloud.dev/runtimevar` referencing the PEM-encoded public key for the account.
	PublicKeyURI string `json:"public_key_uri"`
	// PublicKeyURI is a valid `gocloud.dev/runtimevar` referencing the PEM-encoded private key for the account.
	PrivateKeyURI string `json:"private_key_uri"`
	// ManuallyApproveFollowers is a boolean flag signaling that follower requests need to be manually approved. Note: There are currently no tools or interfaces for handling those approvals.
	ManuallyApproveFollowers bool `json:"manually_approve_followers"`
	// Discoverable is a boolean flag signaling that the account is discoverable.
	Discoverable bool `json:"discoverable"`
	// IconURI is a valid `gocloud.dev/blob` URI (as in the bucket URI + filename) referencing the icon URI for the account.
	IconURI string `json:"icon_uri"`
	// Created is a Unix timestamp of when the account was created.
	Created int64 `json:"created"`
	// LastModified is a Unix timestamp of when the account was last modified.
	LastModified int64 `json:"lastmodified"`
}

Account represents an individual ActivityPub account

func (*Account) AccountURL

func (a *Account) AccountURL(ctx context.Context, uris_table *uris.URIs) *url.URL

func (*Account) Address

func (a *Account) Address(hostname string) string

func (*Account) InboxURL

func (a *Account) InboxURL(ctx context.Context, uris_table *uris.URIs) *url.URL

func (*Account) OutboxURL

func (a *Account) OutboxURL(ctx context.Context, uris_table *uris.URIs) *url.URL

func (*Account) PostURL

func (a *Account) PostURL(ctx context.Context, uris_table *uris.URIs, post *Post) *url.URL

func (*Account) PrivateKey

func (a *Account) PrivateKey(ctx context.Context) (string, error)

func (*Account) PrivateKeyRSA

func (a *Account) PrivateKeyRSA(ctx context.Context) (*rsa.PrivateKey, error)

func (*Account) ProfileResource

func (a *Account) ProfileResource(ctx context.Context, uris_table *uris.URIs) (*ap.Actor, error)

func (*Account) ProfileURL

func (a *Account) ProfileURL(ctx context.Context, uris_table *uris.URIs) *url.URL

func (*Account) PublicKey

func (a *Account) PublicKey(ctx context.Context) (string, error)

func (*Account) PublicKeyRSA

func (a *Account) PublicKeyRSA(ctx context.Context) (*rsa.PublicKey, error)

func (*Account) SendActivity added in v0.0.2

func (a *Account) SendActivity(ctx context.Context, uris_table *uris.URIs, inbox_uri string, activity *ap.Activity) error

func (*Account) String

func (a *Account) String() string

func (*Account) WebfingerResource

func (a *Account) WebfingerResource(ctx context.Context, uris_table *uris.URIs) (*webfinger.Resource, error)

func (*Account) WebfingerURL

func (a *Account) WebfingerURL(ctx context.Context, uris_table *uris.URIs) *url.URL

type AccountType

type AccountType uint32

AccountType denotes the ActivityPub account type

const (

	// PersonType is considered to be an actual human being
	PersonType AccountType
	// ServiceType is considered to be a "bot" or other automated account
	ServiceType
)

func AccountTypeFromString

func AccountTypeFromString(str_type string) (AccountType, error)

AccountTypeFromString returns a known AccountType derived from 'str_type' (an English-language label)

func (AccountType) String

func (t AccountType) String() string

String returns an English-language label for the AccountType

type Activity added in v0.0.2

type Activity struct {
	// A unique 64-bit ID for the activity
	Id int64 `json:"id"`
	// The unique ID associated with the ActivityPub Activity (Body)
	ActivityPubId string `json:"activity_id"`
	// The unique 64-bit ID for account associated with the activity
	AccountId int64 `json:"id"`
	// A valid object type (as in an ActivityPub object) supported by this package
	ActivityType ActivityType `json:"activity_type"`
	// The unique 64-bit ID associated with the activity type (for example if ActivityType is PostActivityType then ActivityId would be unique ID for that post).
	ActivityTypeId int64 `json:"activity_type_id"`
	// The JSON-encoded body of the ActivityPub Activity
	Body string `json:"body"`
	// The Unix timestamp when the activity was created.
	Created int64 `json:"created"`
}

Type Activity is internal representation of an ActivityPub Activity message with pointers to other relevant internal representations of things like account IDs rather than actor URIs, etc.

func NewActivity added in v0.0.2

func NewActivity(ctx context.Context, ap_activity *ap.Activity) (*Activity, error)

NewActivity returns a new `Activity` instance using properties derived from 'ap_activity'.

func (*Activity) UnmarshalActivity added in v0.0.2

func (a *Activity) UnmarshalActivity() (*ap.Activity, error)

UnmarshalActivity returns the unmarshaled `*ap.Activity` that is encapsulated by 'a'.

type ActivityType added in v0.0.2

type ActivityType int
const (
	// UndefinedActivityType is an unknown (or undefined) object type
	UndefinedActivityType ActivityType = iota
	PostActivityType
	BoostActivityType
)

type Alias

type Alias struct {
	Name      string `json:"name"` // Unique primary key
	AccountId int64  `json:"account_id"`
	Created   int64  `json:"created"`
}

func (*Alias) String

func (a *Alias) String() string

type Block

type Block struct {
	Id           int64  `json:"id"`
	AccountId    int64  `json:"account_id"`
	Name         string `json:"name"`
	Host         string `json:"host"`
	Created      int64  `json:"created"`
	LastModified int64  `json:"lastmodified"`
}

func NewBlock

func NewBlock(ctx context.Context, account_id int64, block_host string, block_name string) (*Block, error)

type Boost

type Boost struct {
	Id        int64  `json:"id"`
	AccountId int64  `json:"account_id"`
	PostId    int64  `json:"post_id"`
	Actor     string `json:"actor"`
	Created   int64  `json:"created"`
}

Type Boost is possibly (probably) a misnomer in the same way that type `Post` is (see notes in post.go). Specifically this data and the correspinding `BoostsDatabase` was created to record boosts from external actors about posts created by accounts on this server. It is not currently suited to record or deliver boosts of external posts made by accounts on this server.

func NewBoost

func NewBoost(ctx context.Context, post *Post, actor string) (*Boost, error)

type Boosted added in v0.0.2

type Boosted struct {
	Id        int64  `json:"id"`
	AccountId int64  `json:"account_id"`
	Author    string `json:"author"`
	Object    string `json:"object"`
	Created   int64  `json:"created"`
}

Type Boosted represents an object/URI/thing that a sfomuseum/go-activitypub.Account has boosted. It remains TBD whether this should try to be merged with the `Boost` struct below which would really mean replace `Boost.PostId` with `Boost.Object`...

type Delivery

type Delivery struct {
	Id            int64  `json:"id"`
	ActivityId    int64  `json:"activity_id"`
	ActivityPubId string `json:"activitypub_id"`
	AccountId     int64  `json:"account_id"`
	Recipient     string `json:"recipient"`
	Inbox         string `json:"inbox"`
	Created       int64  `json:"created"`
	Completed     int64  `json:"completed"`
	Success       bool   `json:"success"`
	Error         string `json:"error,omitempty"`
}

type Follower

type Follower struct {
	Id              int64  `json:"id"`
	AccountId       int64  `json:"account_id"`
	FollowerAddress string `json:"follower_address"`
	Created         int64  `json:"created"`
}

func NewFollower

func NewFollower(ctx context.Context, account_id int64, follower_address string) (*Follower, error)

type Following

type Following struct {
	Id               int64  `json:"id"`
	AccountId        int64  `json:"account_id"`
	FollowingAddress string `json:"following_address"`
	Created          int64  `json:"created"`
}

func NewFollowing

func NewFollowing(ctx context.Context, account_id int64, following_address string) (*Following, error)

type Like

type Like struct {
	Id        int64  `json:"id"`
	AccountId int64  `json:"account_id"`
	PostId    int64  `json:"post_id"`
	Actor     string `json:"actor"`
	Created   int64  `json:"created"`
}

Type Like is possibly (probably) a misnomer in the same way that type `Post` is (see notes in post.go and boost.go). Specifically this data and the correspinding `LikesDatabase` was created to record likes from external actors about posts created by accounts on this server. It is not currently suited to record or deliver likes of external posts made by accounts on this server.

func NewLike

func NewLike(ctx context.Context, post *Post, actor string) (*Like, error)

type Message

type Message struct {
	Id            int64  `json:"id"`
	NoteId        int64  `json:"note_id"`
	AuthorAddress string `json:"author_uri"`
	AccountId     int64  `json:"account_id"`
	Created       int64  `json:"created"`
	LastModified  int64  `json:"created"`
}

func NewMessage

func NewMessage(ctx context.Context, account_id int64, note_id int64, author_address string) (*Message, error)

type Note

type Note struct {
	Id            int64  `json:"id"`
	UUID          string `json:"uuid"`
	AuthorAddress string `json:"author_address"`
	Body          string `json:"body"`
	Created       int64  `json:"created"`
	LastModified  int64  `json:"lastmodified"`
}

Note is a message (or post) delivered to an account.

func NewNote

func NewNote(ctx context.Context, uuid string, author string, body string) (*Note, error)

type Post

type Post struct {
	// The unique ID for the post.
	Id int64 `json:"id"`
	// The AccountsDatabase ID of the author of the post.
	AccountId int64 `json:"account_id"`
	// The body of the post. This is a string mostly because []byte thingies get encoded incorrectly
	// in DynamoDB
	Body string `json:"body"`
	// The URL of the post this post is referencing.
	InReplyTo string `json:"in_reply_to"`
	// The Unix timestamp when the post was created
	Created int64 `json:"created"`
	// The Unix timestamp when the post was last modified
	LastModified int64 `json:"lastmodified"`
}

Post is a message (or post) written by an account holder. It is internal representation of what would be delivered as an ActivityPub note.x

func NewPost

func NewPost(ctx context.Context, acct *Account, body string) (*Post, error)

NewPost returns a new `Post` instance from 'acct' and 'body'.

type PostTag

type PostTag struct {
	Id        int64  `json:"id"`
	AccountId int64  `json:"account_id"`
	PostId    int64  `json:"post_id"`
	Href      string `json:"href"`
	Name      string `json:"name"`
	Type      string `json:"type"`
	Created   int64  `json:"created"`
}

func NewMention

func NewMention(ctx context.Context, post *Post, name string, href string) (*PostTag, error)

type Property

type Property struct {
	Id        int64  `json:"id"`
	AccountId int64  `json:"account_id"`
	Key       string `json:"key"`
	Value     string `json:"value"`
	Created   int64  `json:"created"`
}

Property is a single key-value property associated with an account holder.

func NewProperty

func NewProperty(ctx context.Context, acct *Account, k string, v string) (*Property, error)

func (*Property) String

func (pr *Property) String() string

Directories

Path Synopsis
app
aliases/add
Add one or more aliases for a sfomuseum/go-activitypub account.
Add one or more aliases for a sfomuseum/go-activitypub account.
aliases/list
Gather the list of aliases for a given account and emit as JSON-encoded string to STDOUT.
Gather the list of aliases for a given account and emit as JSON-encoded string to STDOUT.
cmd
Package crypto provides methods for generating and processing x509 public and private keys.
Package crypto provides methods for generating and processing x509 public and private keys.
Package database defines interfaces and default implementation for the underlying databases (or database tables) used to store ActivityPub related operations.
Package database defines interfaces and default implementation for the underlying databases (or database tables) used to store ActivityPub related operations.
Package deliver provides methods for delivering activities to external actors.
Package deliver provides methods for delivering activities to external actors.
Package id provides methods for generating unique identifiers.
Package id provides methods for generating unique identifiers.
schema
Package template provides methods and default templates used by this package.
Package template provides methods and default templates used by this package.
html
Package html defines methods and default templates for generating HTML pages.
Package html defines methods and default templates for generating HTML pages.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL