Implements PLAN.md Phase 8. internal/pushrules is the rule language: the spec's default ruleset, all six condition kinds, glob and word-boundary matching, intentional mentions switching off the legacy body rules, and rule ordering as Synapse has it. internal/pushserver keeps the rules, pushers and notifications and talks to push gateways. Push rules are the m.push_rules account data, written only by the push server; reads keep the user's own rules and the enabled/actions of each default and take the rest from the current defaults, so new default rules reach every user without a migration. PUT account_data/m.push_rules is refused with 405. The push server listens on the roomserver's emit stage and evaluates each event on its own worker for every local member and an invite's target. Notifications and per-room unread counts are stored in migration 0010; counts are a seventh sync token stream, so /sync reports unread_notifications and picks up count changes made after the event or by a receipt from another device. m.read and m.read.private receipts clear the counts; leaving a room clears it. Pushers (http kind only) push through a bounded sender pool with retries, honour event_id_only, drop push keys the gateway rejects, and go away with their device. GET /notifications pages the history. New config section push: enabled, include_content, gateway_timeout, notification_retention. Account data writes now wake the user's blocked syncs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
package setup
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/federationapi"
|
|
"github.com/dariuszjaniak/warp/server/internal/keyserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
|
|
"github.com/dariuszjaniak/warp/server/internal/roomserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
"github.com/dariuszjaniak/warp/server/internal/userapi"
|
|
)
|
|
|
|
// fanout is the roomserver's emit stage when more than one thing listens:
|
|
// the sync notifier, which wakes long polls, and the federation queue,
|
|
// which sends events to other servers.
|
|
//
|
|
// Each listener is held to the roomserver's rule for a Notifier — never
|
|
// block — so calling them in turn under the room lock is fine.
|
|
type fanout struct {
|
|
mu sync.RWMutex
|
|
listeners []roomserver.Notifier
|
|
}
|
|
|
|
func (f *fanout) add(n roomserver.Notifier) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.listeners = append(f.listeners, n)
|
|
}
|
|
|
|
// EventPersisted implements roomserver.Notifier.
|
|
func (f *fanout) EventPersisted(ctx context.Context, n roomserver.Notification) {
|
|
f.mu.RLock()
|
|
defer f.mu.RUnlock()
|
|
for _, l := range f.listeners {
|
|
l.EventPersisted(ctx, n)
|
|
}
|
|
}
|
|
|
|
// keyMemberships is the keyserver's listener on the emit stage: a
|
|
// membership ending is when a remote user may stop sharing a room with
|
|
// anybody here, and the keyserver drops its cached copy of their keys once
|
|
// they have. MembershipLeft does not block, as the emit stage requires.
|
|
type keyMemberships struct{ keys *keyserver.API }
|
|
|
|
// EventPersisted implements roomserver.Notifier.
|
|
func (l keyMemberships) EventPersisted(_ context.Context, n roomserver.Notification) {
|
|
if n.Event == nil || n.Event.Type() != event.TypeMember || !n.StateChanged {
|
|
return
|
|
}
|
|
userID, ok := n.Event.StateKey()
|
|
if !ok {
|
|
return
|
|
}
|
|
switch membership, _ := n.Event.Membership(); membership {
|
|
case event.MembershipLeave, event.MembershipBan:
|
|
l.keys.MembershipLeft(userID)
|
|
}
|
|
}
|
|
|
|
// deviceObservers fans the userapi's device changes out to every
|
|
// component that keeps state per device: the keyserver's keys and the push
|
|
// server's pushers.
|
|
type deviceObservers []userapi.DeviceObserver
|
|
|
|
func (o deviceObservers) DevicesDeleted(ctx context.Context, localpart string, deviceIDs []string) {
|
|
for _, observer := range o {
|
|
observer.DevicesDeleted(ctx, localpart, deviceIDs)
|
|
}
|
|
}
|
|
|
|
func (o deviceObservers) DeviceRenamed(ctx context.Context, localpart, deviceID string) {
|
|
for _, observer := range o {
|
|
observer.DeviceRenamed(ctx, localpart, deviceID)
|
|
}
|
|
}
|
|
|
|
// federationStatus adapts the module to the admin API.
|
|
type federationStatus struct {
|
|
module *federationapi.Module
|
|
}
|
|
|
|
func (s federationStatus) Running() bool { return s.module.Running() }
|
|
|
|
func (s federationStatus) Destinations(ctx context.Context) ([]storage.DestinationSummary, error) {
|
|
return s.module.Queue().Destinations(ctx)
|
|
}
|
|
|
|
func (s federationStatus) ResetBackoff(ctx context.Context, serverName string) error {
|
|
return s.module.Queue().ResetBackoff(ctx, serverName)
|
|
}
|
|
|
|
// federationTLS builds the client TLS configuration for outbound
|
|
// federation: the system roots, plus a configured CA bundle.
|
|
func federationTLS(caFile string) (*tls.Config, error) {
|
|
cfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
|
if caFile == "" {
|
|
return cfg, nil
|
|
}
|
|
pool, err := x509.SystemCertPool()
|
|
if err != nil || pool == nil {
|
|
pool = x509.NewCertPool()
|
|
}
|
|
pem, err := os.ReadFile(caFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read federation.tls_ca_file: %w", err)
|
|
}
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("federation.tls_ca_file %s contains no PEM certificates", caFile)
|
|
}
|
|
cfg.RootCAs = pool
|
|
return cfg, nil
|
|
}
|