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>
98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
package roomserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/auth"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
|
|
)
|
|
|
|
// PushRoom is what push rule evaluation needs to know about a room: who is
|
|
// in it under what name, what it is called, and who may notify all of it.
|
|
//
|
|
// It is read from the room's current state rather than the state at the
|
|
// event being evaluated. The push server reads it moments after the event
|
|
// lands, when the two differ only if somebody joined or renamed themselves
|
|
// in between, and reading the state at every event would cost a state
|
|
// resolution per message per room member.
|
|
type PushRoom struct {
|
|
RoomID string
|
|
|
|
// Members maps each joined user to their display name in the room,
|
|
// empty when they have not set one.
|
|
Members map[string]string
|
|
|
|
// Name is m.room.name, and CanonicalAlias m.room.canonical_alias's
|
|
// alias: what a push to a phone calls the room.
|
|
Name string
|
|
CanonicalAlias string
|
|
|
|
powerLevels *auth.PowerLevels
|
|
}
|
|
|
|
// SenderMayNotify reports whether a user has the power a notification key
|
|
// requires — "room", for an @room mention.
|
|
func (p PushRoom) SenderMayNotify(userID, key string) bool {
|
|
if p.powerLevels == nil {
|
|
return false
|
|
}
|
|
return p.powerLevels.UserLevel(userID) >= p.powerLevels.NotificationLevel(key)
|
|
}
|
|
|
|
// PushRoomFor reads a room for the push server.
|
|
//
|
|
// It is a server-side read with no user to check against: the push server
|
|
// evaluates events on behalf of the room's members, every one of whom
|
|
// could read the same state.
|
|
func (r *API) PushRoomFor(ctx context.Context, roomID string) (PushRoom, error) {
|
|
room, rules, err := r.roomAndRules(ctx, roomID)
|
|
if err != nil {
|
|
return PushRoom{}, err
|
|
}
|
|
st, err := r.currentState(ctx, room, rules)
|
|
if err != nil {
|
|
return PushRoom{}, err
|
|
}
|
|
|
|
out := PushRoom{RoomID: roomID, Members: map[string]string{}}
|
|
for key, e := range st {
|
|
if key.Type != event.TypeMember {
|
|
continue
|
|
}
|
|
if membership, _ := e.Membership(); membership != event.MembershipJoin {
|
|
continue
|
|
}
|
|
var content struct {
|
|
DisplayName string `json:"displayname"`
|
|
}
|
|
_ = json.Unmarshal(e.Content(), &content)
|
|
out.Members[key.StateKey] = content.DisplayName
|
|
}
|
|
|
|
if e := st.Get("m.room.name", ""); e != nil {
|
|
var content struct {
|
|
Name string `json:"name"`
|
|
}
|
|
_ = json.Unmarshal(e.Content(), &content)
|
|
out.Name = content.Name
|
|
}
|
|
if e := st.Get("m.room.canonical_alias", ""); e != nil {
|
|
var content struct {
|
|
Alias string `json:"alias"`
|
|
}
|
|
_ = json.Unmarshal(e.Content(), &content)
|
|
out.CanonicalAlias = content.Alias
|
|
}
|
|
|
|
if create := st.Create(); create != nil {
|
|
pl, err := auth.ResolvePowerLevels(st.PowerLevels(), create, rules)
|
|
if err != nil {
|
|
return PushRoom{}, fmt.Errorf("%w: %v", ErrRejected, err)
|
|
}
|
|
out.powerLevels = pl
|
|
}
|
|
return out, nil
|
|
}
|