Add shared inbox support for ActivityPub delivery (#4755)

* feat(ap): add support for shared inboxes to reduce outbound load

* feat(db): refactor ap followers db into followers repository

* fix(ap): use the updated activity library to pull out the shared inbox endpoint

* chore(deps): point at updated build of owncast/activity

* fix(ap): typeless endpoints

* feat(test): update ActivityPub test to support shared inboxes

* chore(test): remove unused variable

* fix: feedback from review. Guard against SSRF/non-HTTPS/local and handle transaction errors
This commit is contained in:
Gabe Kangas
2026-01-22 15:33:22 -08:00
committed by GitHub
parent 7a735e6902
commit de6468ad89
31 changed files with 1033 additions and 410 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.15.0
// sqlc v1.30.0
package db
+2 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.15.0
// sqlc v1.30.0
package db
@@ -20,6 +20,7 @@ type ApAcceptedActivity struct {
type ApFollower struct {
Iri string
Inbox string
SharedInbox sql.NullString
Name sql.NullString
Username string
Image sql.NullString
+8 -5
View File
@@ -9,13 +9,13 @@ SElECT count(*) FROM ap_followers WHERE approved_at is not null;
SElECT count(*) FROM ap_outbox;
-- name: GetFederationFollowersWithOffset :many
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2;
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2;
-- name: GetRejectedAndBlockedFollowers :many
SELECT iri, name, username, image, created_at, disabled_at FROM ap_followers WHERE disabled_at is not null;
-- name: GetFederationFollowerApprovalRequests :many
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null;
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null;
-- name: ApproveFederationFollower :exec
UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2;
@@ -24,7 +24,7 @@ UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2;
UPDATE ap_followers SET approved_at = null, disabled_at = $1 WHERE iri = $2;
-- name: GetFollowerByIRI :one
SELECT iri, inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1;
SELECT iri, inbox, shared_inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1;
-- name: GetOutboxWithOffset :many
SELECT value FROM ap_outbox LIMIT $1 OFFSET $2;
@@ -37,7 +37,7 @@ SELECT value, live_notification, created_at FROM ap_outbox WHERE iri = $1;
DELETE FROM ap_followers WHERE iri = $1;
-- name: AddFollower :exec
INSERT INTO ap_followers(iri, inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8);
INSERT INTO ap_followers(iri, inbox, shared_inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8, $9);
-- name: AddToOutbox :exec
INSERT INTO ap_outbox(iri, value, type, live_notification) values($1, $2, $3, $4);
@@ -55,7 +55,10 @@ SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestam
SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TYPE = $3;
-- name: UpdateFollowerByIRI :exec
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5;
UPDATE ap_followers SET inbox = $1, shared_inbox = $2, name = $3, username = $4, image = $5 WHERE iri = $6;
-- name: GetUniqueDeliveryInboxes :many
SELECT COALESCE(shared_inbox, inbox) as delivery_inbox FROM ap_followers WHERE approved_at is not null GROUP BY delivery_inbox;
-- name: BanIPAddress :exec
INSERT INTO ip_bans(ip_address, notes) values($1, $2);
+66 -30
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.19.1
// sqlc v1.30.0
// source: query.sql
package db
@@ -41,12 +41,13 @@ func (q *Queries) AddAuthForUser(ctx context.Context, arg AddAuthForUserParams)
}
const addFollower = `-- name: AddFollower :exec
INSERT INTO ap_followers(iri, inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8)
INSERT INTO ap_followers(iri, inbox, shared_inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
type AddFollowerParams struct {
Iri string
Inbox string
SharedInbox sql.NullString
Request string
RequestObject []byte
Name sql.NullString
@@ -59,6 +60,7 @@ func (q *Queries) AddFollower(ctx context.Context, arg AddFollowerParams) error
_, err := q.db.ExecContext(ctx, addFollower,
arg.Iri,
arg.Inbox,
arg.SharedInbox,
arg.Request,
arg.RequestObject,
arg.Name,
@@ -158,7 +160,7 @@ UPDATE users SET display_color = $1 WHERE id = $2
`
type ChangeDisplayColorParams struct {
DisplayColor int
DisplayColor int32
ID string
}
@@ -206,16 +208,17 @@ func (q *Queries) DoesInboundActivityExist(ctx context.Context, arg DoesInboundA
}
const getFederationFollowerApprovalRequests = `-- name: GetFederationFollowerApprovalRequests :many
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null
`
type GetFederationFollowerApprovalRequestsRow struct {
Iri string
Inbox string
Name sql.NullString
Username string
Image sql.NullString
CreatedAt sql.NullTime
Iri string
Inbox string
SharedInbox sql.NullString
Name sql.NullString
Username string
Image sql.NullString
CreatedAt sql.NullTime
}
func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]GetFederationFollowerApprovalRequestsRow, error) {
@@ -230,6 +233,7 @@ func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]
if err := rows.Scan(
&i.Iri,
&i.Inbox,
&i.SharedInbox,
&i.Name,
&i.Username,
&i.Image,
@@ -249,21 +253,22 @@ func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]
}
const getFederationFollowersWithOffset = `-- name: GetFederationFollowersWithOffset :many
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2
`
type GetFederationFollowersWithOffsetParams struct {
Limit int
Offset int
Limit int32
Offset int32
}
type GetFederationFollowersWithOffsetRow struct {
Iri string
Inbox string
Name sql.NullString
Username string
Image sql.NullString
CreatedAt sql.NullTime
Iri string
Inbox string
SharedInbox sql.NullString
Name sql.NullString
Username string
Image sql.NullString
CreatedAt sql.NullTime
}
func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetFederationFollowersWithOffsetParams) ([]GetFederationFollowersWithOffsetRow, error) {
@@ -278,6 +283,7 @@ func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetF
if err := rows.Scan(
&i.Iri,
&i.Inbox,
&i.SharedInbox,
&i.Name,
&i.Username,
&i.Image,
@@ -297,7 +303,7 @@ func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetF
}
const getFollowerByIRI = `-- name: GetFollowerByIRI :one
SELECT iri, inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1
SELECT iri, inbox, shared_inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1
`
func (q *Queries) GetFollowerByIRI(ctx context.Context, iri string) (ApFollower, error) {
@@ -306,6 +312,7 @@ func (q *Queries) GetFollowerByIRI(ctx context.Context, iri string) (ApFollower,
err := row.Scan(
&i.Iri,
&i.Inbox,
&i.SharedInbox,
&i.Name,
&i.Username,
&i.Image,
@@ -365,8 +372,8 @@ SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestam
`
type GetInboundActivitiesWithOffsetParams struct {
Limit int
Offset int
Limit int32
Offset int32
}
type GetInboundActivitiesWithOffsetRow struct {
@@ -514,8 +521,8 @@ SELECT value FROM ap_outbox LIMIT $1 OFFSET $2
`
type GetOutboxWithOffsetParams struct {
Limit int
Offset int
Limit int32
Offset int32
}
func (q *Queries) GetOutboxWithOffset(ctx context.Context, arg GetOutboxWithOffsetParams) ([][]byte, error) {
@@ -584,6 +591,33 @@ func (q *Queries) GetRejectedAndBlockedFollowers(ctx context.Context) ([]GetReje
return items, nil
}
const getUniqueDeliveryInboxes = `-- name: GetUniqueDeliveryInboxes :many
SELECT COALESCE(shared_inbox, inbox) as delivery_inbox FROM ap_followers WHERE approved_at is not null GROUP BY delivery_inbox
`
func (q *Queries) GetUniqueDeliveryInboxes(ctx context.Context) ([]string, error) {
rows, err := q.db.QueryContext(ctx, getUniqueDeliveryInboxes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var delivery_inbox string
if err := rows.Scan(&delivery_inbox); err != nil {
return nil, err
}
items = append(items, delivery_inbox)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserByAccessToken = `-- name: GetUserByAccessToken :one
SELECT users.id, display_name, display_color, users.created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes FROM users, user_access_tokens WHERE token = $1 AND users.id = user_id
`
@@ -758,20 +792,22 @@ func (q *Queries) SetUserAsAuthenticated(ctx context.Context, id string) error {
}
const updateFollowerByIRI = `-- name: UpdateFollowerByIRI :exec
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5
UPDATE ap_followers SET inbox = $1, shared_inbox = $2, name = $3, username = $4, image = $5 WHERE iri = $6
`
type UpdateFollowerByIRIParams struct {
Inbox string
Name sql.NullString
Username string
Image sql.NullString
Iri string
Inbox string
SharedInbox sql.NullString
Name sql.NullString
Username string
Image sql.NullString
Iri string
}
func (q *Queries) UpdateFollowerByIRI(ctx context.Context, arg UpdateFollowerByIRIParams) error {
_, err := q.db.ExecContext(ctx, updateFollowerByIRI,
arg.Inbox,
arg.SharedInbox,
arg.Name,
arg.Username,
arg.Image,
+1
View File
@@ -4,6 +4,7 @@
CREATE TABLE IF NOT EXISTS ap_followers (
"iri" TEXT NOT NULL,
"inbox" TEXT NOT NULL,
"shared_inbox" TEXT,
"name" TEXT,
"username" TEXT NOT NULL,
"image" TEXT,