Files
Warp/server/internal/federationapi/server.go
T
tajniak81andClaude Opus 5.5 6aaf554336
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 5: federation
Implements PLAN.md Phase 5: the Server-Server API. Warp joins rooms on
other servers and lets their users join its rooms, invites across servers
with the invitee's server countersigning, rejects remote invites (locally
if the inviter is unreachable), and exchanges events, typing, receipts and
to-device messages with other homeservers — all behind the runtime switch,
which now starts and stops real outbound traffic.

The protocol lives in internal/federation with no knowledge of rooms:
X-Matrix signing (fuzzed parser), the spec's discovery algorithm keeping
the dial address and the TLS name apart, a signed client that refuses
every request while the switch is off or the policy denies the
destination, and a key ring that caches other servers' keys for at most a
week, with optional notaries.

The roomserver takes events from elsewhere through the spec's intake:
signatures and hashes (a hash mismatch keeps the redacted form), the rules
against the event's own auth events, against the state before it — with
/get_missing_events for small gaps and /state_ids for wide ones — and
against the current state, where a failure is a soft failure. Events held
without their state are outliers: a column on roomserver_events, because
the current state of a room joined over federation is almost all
outliers, and excluded from every timeline query. Forks are resolved at
the room's head. Membership across servers is make_join/send_join,
make_leave/send_leave and countersigned invites, in both directions;
m.room.server_acl is enforced on everything another server sends.
History before a remote join is backfilled when /messages pages past it,
stored at negative stream positions.

internal/federationapi is the running component: the inbound endpoints,
and an outbound queue that persists what it owes each destination, sends
it in transactions of 50 PDUs and 100 EDUs, backs off from ten seconds to
six hours, survives restarts, and drops events persisted while federation
is off rather than delivering a backlog in one burst. Its Module is what
the switch starts and stops.

Also: a TLS listener for port 8448, trusted key servers, key validity and
an outbound CA bundle in the config; per-destination health and backoff
reset in the admin API; remote profiles, aliases and directories in the
Client-Server API; a Complement federation profile. See
docs/federation.md, including its known gaps (no partial-state joins, no
key rotation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:14:20 +02:00

662 lines
23 KiB
Go

package federationapi
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/dariuszjaniak/warp/server/internal/buildinfo"
"github.com/dariuszjaniak/warp/server/internal/federation"
"github.com/dariuszjaniak/warp/server/internal/httputil"
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
"github.com/dariuszjaniak/warp/server/internal/roomserver"
"github.com/dariuszjaniak/warp/server/internal/storage"
"github.com/dariuszjaniak/warp/server/internal/syncapi"
)
// maxBody caps a federation request body. A transaction is up to 50 PDUs
// of up to 64 KiB each, plus EDUs; anything past this is not a
// transaction.
const maxBody = 8 << 20
// Error codes the federation API uses beyond the Client-Server set.
const (
errCodeIncompatibleRoomVersion = "M_INCOMPATIBLE_ROOM_VERSION"
errCodeUnableToAuthoriseJoin = "M_UNABLE_TO_AUTHORISE_JOIN"
)
// EphemeralSink takes in EDUs from other servers. *syncapi.API satisfies
// it.
type EphemeralSink interface {
SetTyping(ctx context.Context, req syncapi.TypingRequest) error
SetReceipt(ctx context.Context, req syncapi.ReceiptRequest) error
SendToDevice(ctx context.Context, req syncapi.SendToDeviceRequest) error
}
// Profiles answers profile queries for local users. *userapi.API
// satisfies it.
type Profiles interface {
Account(ctx context.Context, localpart string) (storage.Account, error)
}
// ServerOptions configure the inbound API.
type ServerOptions struct {
ServerName string
Gate *federation.Gate
Keys *federation.KeyRing
Rooms *roomserver.API
Ephemeral EphemeralSink
Profiles Profiles
Store storage.FederationStore
Logger *slog.Logger
Now func() time.Time
}
// Server is the inbound Server-Server API.
type Server struct {
serverName string
gate *federation.Gate
keys *federation.KeyRing
rooms *roomserver.API
ephemeral EphemeralSink
profiles Profiles
store storage.FederationStore
log *slog.Logger
now func() time.Time
}
// NewServer builds the inbound API.
func NewServer(opts ServerOptions) (*Server, error) {
switch {
case opts.ServerName == "":
return nil, errors.New("federationapi: ServerName must not be empty")
case opts.Gate == nil, opts.Keys == nil, opts.Rooms == nil, opts.Store == nil:
return nil, errors.New("federationapi: Gate, Keys, Rooms and Store are required")
}
if opts.Logger == nil {
opts.Logger = slog.Default()
}
if opts.Now == nil {
opts.Now = time.Now
}
return &Server{
serverName: opts.ServerName,
gate: opts.Gate,
keys: opts.Keys,
rooms: opts.Rooms,
ephemeral: opts.Ephemeral,
profiles: opts.Profiles,
store: opts.Store,
log: opts.Logger,
now: opts.Now,
}, nil
}
// authedHandler is a federation handler that runs after the request has
// been authenticated, with the origin and the body it was signed over.
type authedHandler func(w http.ResponseWriter, r *http.Request, origin string, body []byte) error
// RegisterRoutes adds every federation endpoint, each behind the gate: with
// the switch off the whole surface answers M_UNRECOGNIZED, as a server
// with no federation API would.
func (s *Server) RegisterRoutes(r *httputil.Router) {
public := func(pattern string, h httputil.Handler) {
r.Handle(pattern, s.gate.ProtectFunc(h))
}
authed := func(pattern string, h authedHandler) {
r.Handle(pattern, s.gate.ProtectFunc(s.authenticated(h)))
}
// Keys and version need no authentication: a server has to be able to
// fetch our key before it can check anything we sign.
public("GET /_matrix/key/v2/server", s.handleServerKey)
public("GET /_matrix/key/v2/server/{keyID}", s.handleServerKey)
public("POST /_matrix/key/v2/query", s.handleKeyQuery)
public("GET /_matrix/key/v2/query/{serverName}", s.handleKeyQueryOne)
public("GET /_matrix/key/v2/query/{serverName}/{keyID}", s.handleKeyQueryOne)
public("GET /_matrix/federation/v1/version", s.handleVersion)
const v1, v2 = "/_matrix/federation/v1", "/_matrix/federation/v2"
authed("PUT "+v1+"/send/{txnID}", s.handleSend)
authed("GET "+v1+"/make_join/{roomID}/{userID}", s.handleMakeJoin)
authed("PUT "+v1+"/send_join/{roomID}/{eventID}", s.handleSendJoinV1)
authed("PUT "+v2+"/send_join/{roomID}/{eventID}", s.handleSendJoinV2)
authed("GET "+v1+"/make_leave/{roomID}/{userID}", s.handleMakeLeave)
authed("PUT "+v1+"/send_leave/{roomID}/{eventID}", s.handleSendLeaveV1)
authed("PUT "+v2+"/send_leave/{roomID}/{eventID}", s.handleSendLeaveV2)
authed("PUT "+v2+"/invite/{roomID}/{eventID}", s.handleInvite)
authed("GET "+v1+"/event/{eventID}", s.handleEvent)
authed("GET "+v1+"/state/{roomID}", s.handleState)
authed("GET "+v1+"/state_ids/{roomID}", s.handleStateIDs)
authed("GET "+v1+"/event_auth/{roomID}/{eventID}", s.handleEventAuth)
authed("GET "+v1+"/backfill/{roomID}", s.handleBackfill)
authed("POST "+v1+"/get_missing_events/{roomID}", s.handleMissingEvents)
authed("GET "+v1+"/query/directory", s.handleQueryDirectory)
authed("GET "+v1+"/query/profile", s.handleQueryProfile)
authed("GET "+v1+"/publicRooms", s.handlePublicRoomsGet)
authed("POST "+v1+"/publicRooms", s.handlePublicRoomsPost)
}
// authenticated checks X-Matrix and the allow/deny lists before the
// handler runs.
func (s *Server) authenticated(h authedHandler) httputil.Handler {
return func(w http.ResponseWriter, r *http.Request) error {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBody))
if err != nil {
return httputil.NewError(http.StatusRequestEntityTooLarge, httputil.ErrCodeTooLarge, "Request body too large")
}
origin, err := s.keys.AuthenticateRequest(r.Context(), r, body)
if err != nil {
httputil.LoggerFrom(r.Context()).Info("federation request failed authentication", "error", err)
return httputil.NewError(http.StatusUnauthorized, httputil.ErrCodeUnauthorized, "%s", err.Error())
}
if !s.gate.Policy().Allowed(origin) {
return httputil.Forbidden("Federation with %s is not allowed by this server", origin)
}
return h(w, r, origin, body)
}
}
// fedError maps a roomserver error onto the Matrix error a remote server
// is sent.
func fedError(err error) error {
var matrixErr *httputil.MatrixError
switch {
case err == nil:
return nil
case errors.As(err, &matrixErr):
return err
case errors.Is(err, roomserver.ErrIncompatibleRoomVersion):
return httputil.NewError(http.StatusBadRequest, errCodeIncompatibleRoomVersion, "%s", err.Error())
case errors.Is(err, roomserver.ErrNoSuchRoom), errors.Is(err, roomserver.ErrNoSuchEvent),
errors.Is(err, roomserver.ErrNoSuchAlias):
return httputil.NotFound("%s", err.Error())
case errors.Is(err, roomserver.ErrForbidden), errors.Is(err, roomserver.ErrRejected),
errors.Is(err, roomserver.ErrNotInRoom):
return httputil.Forbidden("%s", err.Error())
case errors.Is(err, roomserver.ErrBadEvent), errors.Is(err, roomserver.ErrInvalidContent):
return httputil.BadJSON("%s", err.Error())
case errors.Is(err, roomserver.ErrFederationDisabled):
return httputil.Unrecognized()
case errors.Is(err, roomserver.ErrRemote):
return httputil.NewError(http.StatusBadGateway, httputil.ErrCodeUnknown, "%s", err.Error())
}
return err
}
// decode reads an already-read body.
func decode(body []byte, v any) error {
if len(body) == 0 {
return httputil.NotJSON()
}
if err := json.Unmarshal(body, v); err != nil {
return httputil.BadJSON("Invalid request body: %v", err)
}
return nil
}
// --- keys and version -------------------------------------------------------
func (s *Server) handleServerKey(w http.ResponseWriter, _ *http.Request) error {
doc, err := s.keys.OwnKeyResponse()
if err != nil {
return err
}
httputil.WriteRawJSON(w, http.StatusOK, doc)
return nil
}
// keyQueryResponse is the notary answer.
type keyQueryResponse struct {
ServerKeys []json.RawMessage `json:"server_keys"`
}
func (s *Server) handleKeyQuery(w http.ResponseWriter, r *http.Request) error {
var req struct {
ServerKeys map[string]json.RawMessage `json:"server_keys"`
}
if err := httputil.DecodeJSON(w, r, &req); err != nil {
return err
}
resp := keyQueryResponse{ServerKeys: []json.RawMessage{}}
for serverName := range req.ServerKeys {
docs, err := s.keys.NotaryResponse(serverName)
if err != nil {
return err
}
resp.ServerKeys = append(resp.ServerKeys, docs...)
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
func (s *Server) handleKeyQueryOne(w http.ResponseWriter, r *http.Request) error {
docs, err := s.keys.NotaryResponse(r.PathValue("serverName"))
if err != nil {
return err
}
if docs == nil {
docs = []json.RawMessage{}
}
httputil.WriteJSON(w, http.StatusOK, keyQueryResponse{ServerKeys: docs})
return nil
}
func (s *Server) handleVersion(w http.ResponseWriter, _ *http.Request) error {
var resp federation.VersionResponse
resp.Server.Name = "Warp"
resp.Server.Version = buildinfo.Version
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// --- transactions -------------------------------------------------------------
func (s *Server) handleSend(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
txnID := r.PathValue("txnID")
if cached, ok, err := s.store.InboundTransaction(r.Context(), origin, txnID); err != nil {
return err
} else if ok {
httputil.WriteRawJSON(w, http.StatusOK, cached)
return nil
}
var txn federation.Transaction
if err := decode(body, &txn); err != nil {
return err
}
if txn.Origin != "" && txn.Origin != origin {
return httputil.Forbidden("The transaction's origin is %s, but it was signed by %s", txn.Origin, origin)
}
if len(txn.PDUs) > maxPDUsPerTxn || len(txn.EDUs) > maxEDUsPerTxn {
return httputil.BadJSON("A transaction carries at most %d PDUs and %d EDUs", maxPDUsPerTxn, maxEDUsPerTxn)
}
resp := federation.SendTransactionResponse{PDUs: map[string]federation.PDUResult{}}
for _, raw := range txn.PDUs {
eventID, err := s.rooms.ReceivePDU(r.Context(), origin, raw)
if eventID == "" {
// Unparseable: there is no ID to report a result under.
s.log.Info("dropping an unreadable PDU", "origin", origin, "error", err)
continue
}
result := federation.PDUResult{}
if err != nil {
result.Error = err.Error()
s.log.Info("refused a PDU", "origin", origin, "event_id", eventID, "error", err)
}
resp.PDUs[eventID] = result
}
for _, edu := range txn.EDUs {
s.receiveEDU(r.Context(), origin, edu)
}
encoded, err := json.Marshal(resp)
if err != nil {
return err
}
if err := s.store.RecordInboundTransaction(r.Context(), origin, txnID, encoded, s.now().UnixMilli()); err != nil {
s.log.Warn("could not record an inbound transaction", "origin", origin, "txn_id", txnID, "error", err)
}
httputil.WriteRawJSON(w, http.StatusOK, encoded)
return nil
}
// sameServer reports whether a user belongs to the server that sent the
// EDU about them. Nothing in an EDU is signed, so this is the only check
// there is, and without it any server could make anybody appear to type.
func sameServer(userID, origin string) bool {
server, err := id.ServerNameOf(userID)
return err == nil && server == origin
}
// receiveEDU takes one EDU in. Failures are logged and otherwise ignored:
// an EDU is ephemeral, and the transaction it came in succeeds whatever
// becomes of it.
func (s *Server) receiveEDU(ctx context.Context, origin string, edu federation.EDU) {
if s.ephemeral == nil {
return
}
switch edu.Type {
case federation.EDUTyping:
var c federation.TypingContent
if json.Unmarshal(edu.Content, &c) != nil || !sameServer(c.UserID, origin) || !s.allowed(ctx, c.RoomID, origin) {
return
}
if err := s.ephemeral.SetTyping(ctx, syncapi.TypingRequest{RoomID: c.RoomID, User: c.UserID, Typing: c.Typing}); err != nil {
s.log.Debug("ignored a typing EDU", "origin", origin, "room_id", c.RoomID, "error", err)
}
case federation.EDUReceipt:
var c federation.ReceiptContent
if json.Unmarshal(edu.Content, &c) != nil {
return
}
for roomID, byType := range c {
if !s.allowed(ctx, roomID, origin) {
continue
}
for receiptType, byUser := range byType {
if receiptType != syncapi.ReceiptRead {
continue
}
for userID, data := range byUser {
if !sameServer(userID, origin) || len(data.EventIDs) == 0 {
continue
}
if err := s.ephemeral.SetReceipt(ctx, syncapi.ReceiptRequest{
RoomID: roomID, User: userID, ReceiptType: receiptType,
EventID: data.EventIDs[len(data.EventIDs)-1], ThreadID: data.Data.ThreadID,
}); err != nil {
s.log.Debug("ignored a receipt EDU", "origin", origin, "room_id", roomID, "error", err)
}
}
}
}
case federation.EDUDirectToDev:
var c federation.DirectToDeviceContent
if json.Unmarshal(edu.Content, &c) != nil || !sameServer(c.Sender, origin) {
return
}
if err := s.ephemeral.SendToDevice(ctx, syncapi.SendToDeviceRequest{
Sender: c.Sender, EventType: c.Type, Messages: c.Messages,
}); err != nil {
s.log.Debug("ignored a to-device EDU", "origin", origin, "error", err)
}
default:
// Presence and device list updates belong to phases that have not
// landed (presence is off by default; device lists are Phase 7).
s.log.Debug("ignored an EDU", "origin", origin, "type", edu.Type)
}
}
// --- membership ---------------------------------------------------------------
func (s *Server) handleMakeJoin(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
version, tmpl, err := s.rooms.MakeJoin(r.Context(), origin, r.PathValue("roomID"), r.PathValue("userID"),
r.URL.Query()["ver"])
if err != nil {
if errors.Is(err, roomserver.ErrForbidden) && strings.Contains(err.Error(), "authorise a restricted join") {
return httputil.NewError(http.StatusBadRequest, errCodeUnableToAuthoriseJoin, "%s", err.Error())
}
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.MakeJoinResponse{RoomVersion: version, Event: tmpl})
return nil
}
func (s *Server) sendJoin(r *http.Request, origin string, body []byte) (federation.SendJoinResponse, error) {
res, err := s.rooms.SendJoin(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"), body)
if err != nil {
return federation.SendJoinResponse{}, fedError(err)
}
return federation.SendJoinResponse{
Origin: s.serverName,
State: res.State,
AuthChain: res.AuthChain,
Event: res.Event,
ServersInRoom: res.ServersInRoom,
}, nil
}
func (s *Server) handleSendJoinV2(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
resp, err := s.sendJoin(r, origin, body)
if err != nil {
return err
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// handleSendJoinV1 is the deprecated form, which wraps the same answer in
// a [200, body] pair.
func (s *Server) handleSendJoinV1(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
resp, err := s.sendJoin(r, origin, body)
if err != nil {
return err
}
httputil.WriteJSON(w, http.StatusOK, []any{http.StatusOK, resp})
return nil
}
func (s *Server) handleMakeLeave(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
version, tmpl, err := s.rooms.MakeLeave(r.Context(), origin, r.PathValue("roomID"), r.PathValue("userID"))
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.MakeJoinResponse{RoomVersion: version, Event: tmpl})
return nil
}
func (s *Server) handleSendLeaveV2(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
if err := s.rooms.SendLeave(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"), body); err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, struct{}{})
return nil
}
func (s *Server) handleSendLeaveV1(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
if err := s.rooms.SendLeave(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"), body); err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, []any{http.StatusOK, struct{}{}})
return nil
}
func (s *Server) handleInvite(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
var req federation.InviteRequest
if err := decode(body, &req); err != nil {
return err
}
signed, err := s.rooms.ReceiveInvite(r.Context(), origin, req.RoomVersion, req.Event, req.InviteRoomState)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.InviteResponse{Event: signed})
return nil
}
// --- reads --------------------------------------------------------------------
func (s *Server) transaction(pdus []json.RawMessage) federation.Transaction {
if pdus == nil {
pdus = []json.RawMessage{}
}
return federation.Transaction{Origin: s.serverName, OriginServerTS: s.now().UnixMilli(), PDUs: pdus}
}
func (s *Server) handleEvent(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
raw, err := s.rooms.EventForServer(r.Context(), origin, r.PathValue("eventID"))
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, s.transaction([]json.RawMessage{raw}))
return nil
}
func (s *Server) handleState(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
eventID := r.URL.Query().Get("event_id")
if eventID == "" {
return httputil.MissingParam("event_id is required")
}
state, chain, err := s.rooms.StateForServer(r.Context(), origin, r.PathValue("roomID"), eventID)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.StateResponse{PDUs: state, AuthChain: chain})
return nil
}
func (s *Server) handleStateIDs(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
eventID := r.URL.Query().Get("event_id")
if eventID == "" {
return httputil.MissingParam("event_id is required")
}
state, chain, err := s.rooms.StateIDsForServer(r.Context(), origin, r.PathValue("roomID"), eventID)
if err != nil {
return fedError(err)
}
if state == nil {
state = []string{}
}
if chain == nil {
chain = []string{}
}
httputil.WriteJSON(w, http.StatusOK, federation.StateIDsResponse{PDUIDs: state, AuthChainIDs: chain})
return nil
}
func (s *Server) handleEventAuth(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
chain, err := s.rooms.EventAuthForServer(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"))
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.EventAuthResponse{AuthChain: chain})
return nil
}
func (s *Server) handleBackfill(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
query := r.URL.Query()
from := query["v"]
if len(from) == 0 {
return httputil.MissingParam("v is required")
}
limit, err := strconv.Atoi(query.Get("limit"))
if err != nil || limit <= 0 {
return httputil.InvalidParam("limit must be a positive integer")
}
events, err := s.rooms.BackfillForServer(r.Context(), origin, r.PathValue("roomID"), from, limit)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, s.transaction(events))
return nil
}
func (s *Server) handleMissingEvents(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
var req federation.MissingEventsRequest
if err := decode(body, &req); err != nil {
return err
}
events, err := s.rooms.MissingEventsForServer(r.Context(), origin, r.PathValue("roomID"),
req.EarliestEvents, req.LatestEvents, req.Limit, req.MinDepth)
if err != nil {
return fedError(err)
}
if events == nil {
events = []json.RawMessage{}
}
httputil.WriteJSON(w, http.StatusOK, federation.MissingEventsResponse{Events: events})
return nil
}
// --- queries ------------------------------------------------------------------
func (s *Server) handleQueryDirectory(w http.ResponseWriter, r *http.Request, _ string, _ []byte) error {
alias := r.URL.Query().Get("room_alias")
if alias == "" {
return httputil.MissingParam("room_alias is required")
}
resolved, err := s.rooms.QueryAliasForServer(r.Context(), alias)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.DirectoryResponse{RoomID: resolved.RoomID, Servers: resolved.Servers})
return nil
}
func (s *Server) handleQueryProfile(w http.ResponseWriter, r *http.Request, _ string, _ []byte) error {
query := r.URL.Query()
userID := query.Get("user_id")
localpart, server, err := id.SplitUserID(userID)
if err != nil {
return httputil.InvalidParam("user_id is not a user ID")
}
if server != s.serverName || s.profiles == nil {
return httputil.NotFound("%s is not a user of this server", userID)
}
account, err := s.profiles.Account(r.Context(), localpart)
if errors.Is(err, storage.ErrNoSuchUser) {
return httputil.NotFound("Profile not found")
} else if err != nil {
return err
}
resp := map[string]string{}
switch field := query.Get("field"); field {
case "":
if account.DisplayName != "" {
resp["displayname"] = account.DisplayName
}
if account.AvatarURL != "" {
resp["avatar_url"] = account.AvatarURL
}
case "displayname":
if account.DisplayName != "" {
resp["displayname"] = account.DisplayName
}
case "avatar_url":
if account.AvatarURL != "" {
resp["avatar_url"] = account.AvatarURL
}
default:
return httputil.InvalidParam("field must be displayname or avatar_url")
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
func (s *Server) handlePublicRoomsGet(w http.ResponseWriter, r *http.Request, _ string, _ []byte) error {
query := r.URL.Query()
limit, _ := strconv.Atoi(query.Get("limit"))
return s.publicRooms(w, r, roomserver.PublicRoomsRequest{Limit: limit, Since: query.Get("since")})
}
func (s *Server) handlePublicRoomsPost(w http.ResponseWriter, r *http.Request, _ string, body []byte) error {
var req struct {
Limit int `json:"limit"`
Since string `json:"since"`
Filter struct {
GenericSearchTerm string `json:"generic_search_term"`
} `json:"filter"`
}
if len(body) > 0 {
if err := decode(body, &req); err != nil {
return err
}
}
return s.publicRooms(w, r, roomserver.PublicRoomsRequest{
Limit: req.Limit, Since: req.Since, Search: req.Filter.GenericSearchTerm,
})
}
func (s *Server) publicRooms(w http.ResponseWriter, r *http.Request, req roomserver.PublicRoomsRequest) error {
resp, err := s.rooms.PublicRooms(r.Context(), req)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// String is for logs.
func (s *Server) String() string { return fmt.Sprintf("federation API for %s", s.serverName) }
// allowed applies a room's server ACL to an EDU about it. An EDU is not
// a room event, but it is still a server speaking in the room, and a
// server the room has banned does not get to make its users appear to
// type.
func (s *Server) allowed(ctx context.Context, roomID, origin string) bool {
ok, err := s.rooms.ServerAllowed(ctx, roomID, origin)
return err == nil && ok
}