CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Storage tests on PostgreSQL (push) Waiting to run
CI / Complement (federation off) (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / Smoke test the binary (push) Waiting to run
CI / Admin panel (push) Waiting to run
CI / Web client (push) Waiting to run
CI / Desktop client (ubuntu-24.04) (push) Waiting to run
CI / Desktop client (windows-latest) (push) Waiting to run
The pocketbase driver no longer embeds PocketBase: Warp keeps its data as PocketBase collections on a server of its own and reaches it only through the REST API, the way the postgres driver reaches PostgreSQL. database.connection_string is the server's URL, and Warp signs in as the superuser in WARP_PB_SUPERUSER_EMAIL / WARP_PB_SUPERUSER_PASSWORD (or a WARP_PB_SUPERUSER_FILE of the two). Every SQL table is a superuser-only collection of the same name and columns, created by warp migrate. Reads go through PocketBase's SQL endpoint with the SQL drivers' queries, via a read-only database/sql driver that inlines arguments, moves long ones into warp_params and pages past 1000 rows. Writes are record operations in batch requests under one write lock in the Warp process. Media is a protected file field in warp_media, local or in the S3 bucket Warp writes into PocketBase's settings. Search matches word prefixes with LIKE. The storage tests run against an in-process PocketBase and pass as on SQLite. Docker: the PocketBase stacks get a pocketbase service running tajniak81/pocketbase-docker-root (WARP_PB_IMAGE), with a pb-data volume and its admin UI on loopback; the AIO images run that PocketBase binary as a process beside the homeserver, making up a superuser when none is set. The .env examples, READMEs, docs and warp.yaml.example follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
248 lines
8.1 KiB
Go
248 lines
8.1 KiB
Go
package pocketbase
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
)
|
|
|
|
// Federation is the PocketBase implementation of storage.FederationStore.
|
|
type Federation struct{ *base }
|
|
|
|
// StoreServerKeys implements storage.FederationStore.
|
|
func (s *Federation) StoreServerKeys(ctx context.Context, keys []storage.ServerKey) error {
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
return s.write(ctx, func(t *tx) error {
|
|
for _, k := range keys {
|
|
t.put("federation_server_keys", rec{
|
|
"server_name": k.ServerName, "key_id": k.KeyID, "public_key": k.PublicKey,
|
|
"valid_until_ts": k.ValidUntilTS, "expired_ts": k.ExpiredTS, "fetched_at_ms": k.FetchedAtMS,
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ServerKeys implements storage.FederationStore.
|
|
func (s *Federation) ServerKeys(ctx context.Context, serverName string, keyIDs []string) ([]storage.ServerKey, error) {
|
|
query := `
|
|
SELECT server_name, key_id, public_key, valid_until_ts, expired_ts, fetched_at_ms
|
|
FROM federation_server_keys WHERE server_name = ?`
|
|
args := []any{serverName}
|
|
if len(keyIDs) > 0 {
|
|
query += ` AND key_id IN ` + inList
|
|
args = append(args, jsonList(keyIDs))
|
|
}
|
|
query += ` ORDER BY key_id`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read the keys of %q: %w", serverName, err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var out []storage.ServerKey
|
|
for rows.Next() {
|
|
var k storage.ServerKey
|
|
if err := rows.Scan(&k.ServerName, &k.KeyID, &k.PublicKey,
|
|
&k.ValidUntilTS, &k.ExpiredTS, &k.FetchedAtMS); err != nil {
|
|
return nil, fmt.Errorf("read the keys of %q: %w", serverName, err)
|
|
}
|
|
out = append(out, k)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Enqueue implements storage.FederationStore.
|
|
func (s *Federation) Enqueue(ctx context.Context, items []storage.QueuedItem) ([]storage.QueuedItem, error) {
|
|
if len(items) == 0 {
|
|
return nil, nil
|
|
}
|
|
out := make([]storage.QueuedItem, len(items))
|
|
copy(out, items)
|
|
err := s.write(ctx, func(t *tx) error {
|
|
top, err := t.advance(federationSequences, seqQueue, int64(len(out)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
first := top - int64(len(out)) + 1
|
|
for i := range out {
|
|
out[i].ID = first + int64(i)
|
|
item := out[i]
|
|
t.put("federation_queue", rec{
|
|
"queue_id": item.ID, "destination": item.Destination, "kind": item.Kind,
|
|
"room_id": item.RoomID, "event_id": item.EventID,
|
|
"payload": string(item.Payload), "queued_at_ms": item.QueuedAtMS,
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("enqueue %d items: %w", len(items), err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Queued implements storage.FederationStore.
|
|
func (s *Federation) Queued(ctx context.Context, destination, kind string, limit int) ([]storage.QueuedItem, error) {
|
|
if limit <= 0 {
|
|
return nil, nil
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT queue_id, destination, kind, room_id, event_id, payload, queued_at_ms
|
|
FROM federation_queue
|
|
WHERE destination = ? AND kind = ?
|
|
ORDER BY queue_id
|
|
LIMIT ?`, destination, kind, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read the queue for %q: %w", destination, err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var out []storage.QueuedItem
|
|
for rows.Next() {
|
|
var (
|
|
item storage.QueuedItem
|
|
payload string
|
|
)
|
|
if err := rows.Scan(&item.ID, &item.Destination, &item.Kind, &item.RoomID,
|
|
&item.EventID, &payload, &item.QueuedAtMS); err != nil {
|
|
return nil, fmt.Errorf("read the queue for %q: %w", destination, err)
|
|
}
|
|
item.Payload = []byte(payload)
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Dequeue implements storage.FederationStore.
|
|
func (s *Federation) Dequeue(ctx context.Context, ids []int64) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
err := s.write(ctx, func(t *tx) error {
|
|
_, err := t.delWhere("federation_queue", `queue_id IN `+inList, jsonInts(ids))
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("dequeue %d items: %w", len(ids), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// QueuedDestinations implements storage.FederationStore.
|
|
func (s *Federation) QueuedDestinations(ctx context.Context) ([]string, error) {
|
|
out, err := s.queryStrings(ctx, `SELECT DISTINCT destination FROM federation_queue ORDER BY destination`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read the queued destinations: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
const destinationColumns = `
|
|
server_name, failures, retry_at_ms, last_success_ms, last_failure_ms, last_error`
|
|
|
|
// Destination implements storage.FederationStore.
|
|
func (s *Federation) Destination(ctx context.Context, serverName string) (storage.Destination, error) {
|
|
var d storage.Destination
|
|
err := s.db.QueryRowContext(ctx, `SELECT `+destinationColumns+`
|
|
FROM federation_destinations WHERE server_name = ?`, serverName).
|
|
Scan(&d.ServerName, &d.Failures, &d.RetryAtMS, &d.LastSuccessMS, &d.LastFailureMS, &d.LastError)
|
|
switch {
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
return storage.Destination{ServerName: serverName}, nil
|
|
case err != nil:
|
|
return storage.Destination{}, fmt.Errorf("read destination %q: %w", serverName, err)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// SetDestination implements storage.FederationStore.
|
|
func (s *Federation) SetDestination(ctx context.Context, d storage.Destination) error {
|
|
err := s.write(ctx, func(t *tx) error {
|
|
t.put("federation_destinations", rec{
|
|
"server_name": d.ServerName, "failures": d.Failures, "retry_at_ms": d.RetryAtMS,
|
|
"last_success_ms": d.LastSuccessMS, "last_failure_ms": d.LastFailureMS, "last_error": d.LastError,
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("record destination %q: %w", d.ServerName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Destinations implements storage.FederationStore: every server with a
|
|
// health row or something queued.
|
|
func (s *Federation) Destinations(ctx context.Context) ([]storage.DestinationSummary, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT n.server_name,
|
|
COALESCE(d.failures, 0), COALESCE(d.retry_at_ms, 0),
|
|
COALESCE(d.last_success_ms, 0), COALESCE(d.last_failure_ms, 0),
|
|
COALESCE(d.last_error, ''),
|
|
(SELECT COUNT(*) FROM federation_queue q WHERE q.destination = n.server_name AND q.kind = 'pdu'),
|
|
(SELECT COUNT(*) FROM federation_queue q WHERE q.destination = n.server_name AND q.kind = 'edu')
|
|
FROM (
|
|
SELECT server_name FROM federation_destinations
|
|
UNION
|
|
SELECT DISTINCT destination FROM federation_queue
|
|
) n
|
|
LEFT JOIN federation_destinations d ON d.server_name = n.server_name
|
|
ORDER BY n.server_name`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read the destinations: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var out []storage.DestinationSummary
|
|
for rows.Next() {
|
|
var d storage.DestinationSummary
|
|
if err := rows.Scan(&d.ServerName, &d.Failures, &d.RetryAtMS, &d.LastSuccessMS,
|
|
&d.LastFailureMS, &d.LastError, &d.QueuedPDUs, &d.QueuedEDUs); err != nil {
|
|
return nil, fmt.Errorf("read the destinations: %w", err)
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// InboundTransaction implements storage.FederationStore.
|
|
func (s *Federation) InboundTransaction(ctx context.Context, origin, txnID string) ([]byte, bool, error) {
|
|
var response string
|
|
err := s.db.QueryRowContext(ctx, `SELECT response_json FROM federation_inbound_txns
|
|
WHERE origin = ? AND txn_id = ?`, origin, txnID).Scan(&response)
|
|
switch {
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
return nil, false, nil
|
|
case err != nil:
|
|
return nil, false, fmt.Errorf("read transaction %q from %q: %w", txnID, origin, err)
|
|
}
|
|
return []byte(response), true, nil
|
|
}
|
|
|
|
// RecordInboundTransaction implements storage.FederationStore. A
|
|
// transaction already recorded keeps its first response.
|
|
func (s *Federation) RecordInboundTransaction(ctx context.Context, origin, txnID string, response []byte, atMS int64) error {
|
|
err := s.write(ctx, func(t *tx) error {
|
|
found, err := t.exists("federation_inbound_txns", `origin = ? AND txn_id = ?`, origin, txnID)
|
|
if err != nil || found {
|
|
return err
|
|
}
|
|
t.put("federation_inbound_txns", rec{
|
|
"origin": origin, "txn_id": txnID, "response_json": string(response), "received_at_ms": atMS,
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("record transaction %q from %q: %w", txnID, origin, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ storage.FederationStore = (*Federation)(nil)
|