Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions FEDERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ By default, tootik uses `draft-cavage-http-signatures` when it signs outgoing re
* When it accepts a RFC9421-signed (with or without Ed25519) request from another server, it assumes this server also supports incoming requests signed like this
* It does **not** implement ['double-knocking'](https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions) to detect RFC9421 support, because it's uncommon and this mechanism is very likely to double the number of outgoing requests; instead, tootik randomly (see `RFC9421Threshold` and `Ed25519Threshold`) tries RFC9421 and Ed25519 in `POST` requests to servers that still haven't advertised or demonstrated support, to prevent deadlock if these servers are waiting too

## Collections

tootik sets the `inbox`, `outbox` and `followers` attributes on users.

`POST` requests to `inbox` submit an activity for processing. Outgoing `GET` requests triggered by such a request (for example, to fetch the sending actor or activity from its origin) are signed using one of the the user's keys.

`POST` requests to `outbox` submit an activity for processing, but must carry a valid [FEP-8b32](https://codeberg.org/fediverse/fep/src/branch/main/fep/8b32/fep-8b32.md) integrity proof generated using the user's Ed25519 key.

`GET` requests to `inbox`, `outbox` and `followers` must be signed by the user, otherwise they fail with `404 Not Found`.

`inbox` returns activities delivered to the user's `inbox` and public activities delivered to any other `inbox`.

`outbox` returns activities by the user and other actors that share the same DID (see [Data Portability](#data-portability)).

`followers` returns the user's list of followers.

## Application Actor

tootik creates a special user named `actor`, which acts as an [Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md). Its key is used to sign outgoing requests not initiated by a particular user.
Expand Down Expand Up @@ -105,10 +121,6 @@ To reduce the number of outgoing requests, tootik doesn't fetch a forwarded acti

tootik does not fetch missing posts to complete threads with "ghost replies".

## Outbox

tootik sets the `outbox` attribute on users, but it always leads to an empty collection.

## Account Migration

tootik supports [Mastodon's account migration mechanism](https://docs.joinmastodon.org/spec/activitypub/#Move), but ignores `Move` activities. Account migration is handled by a periodic job. If a user follows a federated user with the `movedTo` attribute set and the new account's `alsoKnownAs` attribute points back to the old account, this job sends follow requests to the new user and cancels old ones.
Expand Down Expand Up @@ -235,7 +247,7 @@ When tootik receives a `POST` request to `inbox` from a portable actor, it requi

tootik validates the integrity proof using the Ed25519 public key extracted from the key ID, and doesn't need to fetch the actor first.

tootik's `inbox` doesn't validate HTTP signatures and simply ignores them. Other servers might do the same, therefore automatic detection of RFC9421 and Ed25519 support on other servers ignores `200 OK` or `202 Accepted` responses from `/.well-known/apgateway`.
tootik's `inbox` doesn't validate HTTP signatures and simply ignores them when the sender is a portable actor. Other servers might do the same, therefore automatic detection of RFC9421 and Ed25519 support on other servers ignores `200 OK` or `202 Accepted` responses from `/.well-known/apgateway`.

tootik forwards posts by actors that share the same DID with a local actor, and replies in threads started by such actors.

Expand All @@ -251,17 +263,6 @@ tootik forwards activites by a portable actor to all actors that share the same

When tootik forwards activities, it assumes that other servers use the same URL format: for example, if the `inbox` property of `[email protected]` is `https://a.localdomain/.well-known/apgateway/did:key:z6MksgCbQa3BZxBayRRkF1hcP7zt6TZGvZF2rR1k3AY7zFL8/actor/inbox` and it forwards an activity to `[email protected]`, it sends a `POST` request to `https://b.localdomain/.well-known/apgateway/did:key:z6MksgCbQa3BZxBayRRkF1hcP7zt6TZGvZF2rR1k3AY7zFL8/actor/inbox`.

tootik's activities export feature exports activities by all actors that share the same canonical ID as the user. The exported activities carry integrity proofs and tootik doesn't require a valid HTTP signature, therefore they can be "imported" into a tootik server by sending them to `inbox`:

```python
import csv
import requests

with open('export.csv') as f:
for r in csv.DictReader(f):
requests.post("https://b.localdomain/.well-known/apgateway/did:key:z6MksgCbQa3BZxBayRRkF1hcP7zt6TZGvZF2rR1k3AY7zFL8/actor/inbox", data=r["Activity"].encode('utf-8'))
```

## Limitations

* tootik does not support `ap://` identifiers, location hints and delivery to `outbox`.
Expand Down
48 changes: 48 additions & 0 deletions ap/collection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright 2025, 2026 Dima Krasner

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ap

type (
CollectionType string
CollectionPageType string
)

const (
OrderedCollection CollectionType = "OrderedCollection"
OrderedCollectionPage CollectionPageType = "OrderedCollectionPage"
)

// Collection represents an ActivityPub collection.
type Collection struct {
Context any `json:"@context"`
ID string `json:"id"`
Type CollectionType `json:"type"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
TotalItems *int64 `json:"totalItems,omitempty"`
OrderedItems any `json:"orderedItems,omitzero"`
}

// CollectionPage represents a [Collection] page.
type CollectionPage struct {
Context any `json:"@context"`
ID string `json:"id"`
Type CollectionPageType `json:"type"`
Next string `json:"next,omitempty"`
Prev string `json:"prev,omitempty"`
PartOf string `json:"partOf,omitempty"`
OrderedItems any `json:"orderedItems,omitzero"`
}
4 changes: 2 additions & 2 deletions ap/inbox.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2025 Dima Krasner
Copyright 2025, 2026 Dima Krasner

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,5 +39,5 @@ type Inbox interface {
UpdateActor(ctx context.Context, actor *Actor, key httpsig.Key) error
UpdateNote(ctx context.Context, actor *Actor, key httpsig.Key, note *Object) error
Unfollow(ctx context.Context, follower *Actor, key httpsig.Key, followed, followID string) error
ProcessActivity(ctx context.Context, tx *sql.Tx, sender *Actor, activity *Activity, rawActivity string, depth int, shared bool) error
ProcessActivity(ctx context.Context, tx *sql.Tx, path sql.NullString, sender *Actor, activity *Activity, rawActivity string, depth int, shared bool) error
}
18 changes: 17 additions & 1 deletion cfg/cfg.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2023 - 2025 Dima Krasner
Copyright 2023 - 2026 Dima Krasner

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -124,6 +124,7 @@ type Config struct {
DeliveryTTL time.Duration
ActorTTL time.Duration
FeedTTL time.Duration
HistoryTTL time.Duration

FillNodeInfoUsage bool

Expand All @@ -132,6 +133,9 @@ type Config struct {

DisableIntegrityProofs bool
MaxGateways int

InboxPageSize int
OutboxPageSize int
}

var defaultMaxInvitationsPerUser = 5
Expand Down Expand Up @@ -425,6 +429,10 @@ func (c *Config) FillDefaults() {
c.FeedTTL = time.Hour * 24 * 7
}

if c.HistoryTTL <= 0 {
c.HistoryTTL = time.Hour * 24 * 30
}

if c.RFC9421Threshold <= 0 || c.RFC9421Threshold > 1 {
c.RFC9421Threshold = 0.95
}
Expand All @@ -436,4 +444,12 @@ func (c *Config) FillDefaults() {
if c.MaxGateways <= 0 {
c.MaxGateways = 10
}

if c.InboxPageSize <= 0 {
c.InboxPageSize = 100
}

if c.OutboxPageSize <= 0 {
c.OutboxPageSize = 100
}
}
Loading
Loading