Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Commit e741b84

Browse files
committed
Add a pushrules module and use it for pushserver filtering.
1 parent 4ff4502 commit e741b84

File tree

10 files changed

+828
-23
lines changed

10 files changed

+828
-23
lines changed

Diff for: pushserver/consumers/roomserver.go

+111-23
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/matrix-org/dendrite/internal"
1111
"github.com/matrix-org/dendrite/internal/pushgateway"
1212
"github.com/matrix-org/dendrite/pushserver/api"
13+
"github.com/matrix-org/dendrite/pushserver/internal/pushrules"
1314
"github.com/matrix-org/dendrite/pushserver/storage"
1415
rsapi "github.com/matrix-org/dendrite/roomserver/api"
1516
"github.com/matrix-org/dendrite/setup/config"
@@ -96,7 +97,7 @@ func (s *OutputRoomEventConsumer) processMessage(ctx context.Context, ore *rsapi
9697
"event_type": event.Type(),
9798
}).Infof("Received event from room server: %#v", ore)
9899

99-
members, err := s.localRoomMembers(ctx, event.RoomID())
100+
members, roomSize, err := s.localRoomMembers(ctx, event.RoomID())
100101
if err != nil {
101102
return err
102103
}
@@ -112,10 +113,10 @@ func (s *OutputRoomEventConsumer) processMessage(ctx context.Context, ore *rsapi
112113
// TODO: does it have to be set? It's not required, and
113114
// removing it means we can send all notifications to
114115
// e.g. Element's Push gateway in one go.
115-
for _, localpart := range members {
116-
if err := s.notifyLocal(ctx, event, localpart); err != nil {
116+
for _, mem := range members {
117+
if err := s.notifyLocal(ctx, event, mem, roomSize); err != nil {
117118
log.WithFields(log.Fields{
118-
"localpart": localpart,
119+
"localpart": mem.Localpart,
119120
}).WithError(err).Errorf("Unable to evaluate push rules")
120121
continue
121122
}
@@ -124,8 +125,15 @@ func (s *OutputRoomEventConsumer) processMessage(ctx context.Context, ore *rsapi
124125
return nil
125126
}
126127

127-
// localRoomMembers fetches the current local members of a room.
128-
func (s *OutputRoomEventConsumer) localRoomMembers(ctx context.Context, roomID string) ([]string, error) {
128+
type localMembership struct {
129+
gomatrixserverlib.MemberContent
130+
UserID string
131+
Localpart string
132+
}
133+
134+
// localRoomMembers fetches the current local members of a room, and
135+
// the total number of members.
136+
func (s *OutputRoomEventConsumer) localRoomMembers(ctx context.Context, roomID string) ([]*localMembership, int, error) {
129137
req := &rsapi.QueryMembershipsForRoomRequest{
130138
RoomID: roomID,
131139
JoinedOnly: true,
@@ -135,24 +143,27 @@ func (s *OutputRoomEventConsumer) localRoomMembers(ctx context.Context, roomID s
135143
// XXX: This could potentially race if the state for the event is not known yet
136144
// e.g. the event came over federation but we do not have the full state persisted.
137145
if err := s.rsAPI.QueryMembershipsForRoom(ctx, req, &res); err != nil {
138-
return nil, err
146+
return nil, 0, err
139147
}
140148

141-
var members []string
149+
var members []*localMembership
150+
var ntotal int
142151
for _, event := range res.JoinEvents {
143152
if event.StateKey == nil {
144153
continue
145154
}
146155

147-
var member gomatrixserverlib.MemberContent
148-
if err := json.Unmarshal(event.Content, &member); err != nil {
156+
var member localMembership
157+
if err := json.Unmarshal(event.Content, &member.MemberContent); err != nil {
149158
log.WithError(err).Errorf("Parsing MemberContent")
150159
continue
151160
}
152161
if member.Membership != gomatrixserverlib.Join {
153162
continue
154163
}
155164

165+
ntotal++
166+
156167
localpart, domain, err := gomatrixserverlib.SplitID('@', *event.StateKey)
157168
if err != nil {
158169
log.WithFields(log.Fields{
@@ -164,37 +175,39 @@ func (s *OutputRoomEventConsumer) localRoomMembers(ctx context.Context, roomID s
164175
continue
165176
}
166177

167-
members = append(members, localpart)
178+
member.UserID = *event.StateKey
179+
member.Localpart = localpart
180+
members = append(members, &member)
168181
}
169182

170-
return members, nil
183+
return members, ntotal, nil
171184
}
172185

173186
// notifyLocal finds the right push actions for a local user, given an event.
174-
func (s *OutputRoomEventConsumer) notifyLocal(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, localpart string) error {
175-
ok, tweaks, err := s.evaluatePushRules(ctx, event, localpart)
187+
func (s *OutputRoomEventConsumer) notifyLocal(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, mem *localMembership, roomSize int) error {
188+
ok, tweaks, err := s.evaluatePushRules(ctx, event, mem, roomSize)
176189
if err != nil {
177190
return err
178191
} else if !ok {
179192
return nil
180193
}
181194

182-
devicesByURL, err := s.localPushDevices(ctx, localpart, tweaks)
195+
devicesByURL, err := s.localPushDevices(ctx, mem.Localpart, tweaks)
183196
if err != nil {
184197
return err
185198
}
186199

187200
log.WithFields(log.Fields{
188201
"room_id": event.RoomID(),
189-
"localpart": localpart,
202+
"localpart": mem.Localpart,
190203
"num_urls": len(devicesByURL),
191204
}).Infof("Notifying push gateways")
192205

193206
var rejected []*pushgateway.Device
194207
for url, devices := range devicesByURL {
195208
log.WithFields(log.Fields{
196209
"room_id": event.RoomID(),
197-
"localpart": localpart,
210+
"localpart": mem.Localpart,
198211
"url": url,
199212
}).Infof("Notifying push gateway")
200213

@@ -203,29 +216,104 @@ func (s *OutputRoomEventConsumer) notifyLocal(ctx context.Context, event *gomatr
203216
continue
204217
}
205218

206-
rej, err := s.notifyHTTP(ctx, event, url, devices, localpart)
219+
rej, err := s.notifyHTTP(ctx, event, url, devices, mem.Localpart)
207220
if err != nil {
208221
log.WithFields(log.Fields{
209222
"event_id": event.EventID(),
210-
"localpart": localpart,
223+
"localpart": mem.Localpart,
211224
}).WithError(err).Errorf("Unable to notify HTTP pusher")
212225
continue
213226
}
214227
rejected = append(rejected, rej...)
215228
}
216229

217230
if len(rejected) > 0 {
218-
return s.deleteRejectedPushers(ctx, rejected, localpart)
231+
return s.deleteRejectedPushers(ctx, rejected, mem.Localpart)
219232
}
220233

221234
return nil
222235
}
223236

224237
// evaluatePushRules fetches and evaluates the push rules of a local
225238
// user. Returns true if the event should be pushed.
226-
func (s *OutputRoomEventConsumer) evaluatePushRules(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, localpart string) (ok bool, tweaks map[string]interface{}, err error) {
227-
// TODO: evaluate push rules
228-
return true, nil, nil
239+
func (s *OutputRoomEventConsumer) evaluatePushRules(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, mem *localMembership, roomSize int) (bool, map[string]interface{}, error) {
240+
if event.Sender() == mem.UserID {
241+
// SPEC: Homeservers MUST NOT notify the Push Gateway for
242+
// events that the user has sent themselves.
243+
return false, nil, nil
244+
}
245+
246+
// TODO: fetch the user's push rules.
247+
ruleSet := pushrules.DefaultRuleSet(mem.Localpart, s.cfg.Matrix.ServerName)
248+
249+
ec := &ruleSetEvalContext{
250+
ctx: ctx,
251+
rsAPI: s.rsAPI,
252+
mem: mem,
253+
roomID: event.RoomID(),
254+
roomSize: roomSize,
255+
}
256+
eval := pushrules.NewRuleSetEvaluator(ec, ruleSet)
257+
rule, err := eval.MatchEvent(event.Event)
258+
if err != nil {
259+
return false, nil, err
260+
}
261+
if rule == nil {
262+
// SPEC: If no rules match an event, the homeserver MUST NOT
263+
// notify the Push Gateway for that event.
264+
return false, nil, err
265+
}
266+
267+
log.WithFields(log.Fields{
268+
"room_id": event.RoomID(),
269+
"localpart": mem.Localpart,
270+
"rule_id": rule.RuleID,
271+
}).Infof("Matched a push rule")
272+
273+
a, tweaks, err := pushrules.ActionsToTweaks(rule.Actions)
274+
if err != nil {
275+
return false, nil, err
276+
}
277+
278+
// TODO: support coalescing.
279+
return a == pushrules.NotifyAction || a == pushrules.CoalesceAction, tweaks, nil
280+
}
281+
282+
type ruleSetEvalContext struct {
283+
ctx context.Context
284+
rsAPI rsapi.RoomserverInternalAPI
285+
mem *localMembership
286+
roomID string
287+
roomSize int
288+
}
289+
290+
func (rse *ruleSetEvalContext) UserDisplayName() string { return rse.mem.DisplayName }
291+
292+
func (rse *ruleSetEvalContext) RoomMemberCount() (int, error) { return rse.roomSize, nil }
293+
294+
func (rse *ruleSetEvalContext) HasPowerLevel(userID, levelKey string) (bool, error) {
295+
req := &rsapi.QueryLatestEventsAndStateRequest{
296+
RoomID: rse.roomID,
297+
StateToFetch: []gomatrixserverlib.StateKeyTuple{
298+
{EventType: "m.room.power_levels"},
299+
},
300+
}
301+
var res rsapi.QueryLatestEventsAndStateResponse
302+
if err := rse.rsAPI.QueryLatestEventsAndState(rse.ctx, req, &res); err != nil {
303+
return false, err
304+
}
305+
for _, ev := range res.StateEvents {
306+
if ev.Type() != gomatrixserverlib.MRoomPowerLevels {
307+
continue
308+
}
309+
310+
plc, err := gomatrixserverlib.NewPowerLevelContentFromEvent(ev.Event)
311+
if err != nil {
312+
return false, err
313+
}
314+
return plc.UserLevel(userID) >= plc.NotificationLevel(levelKey), nil
315+
}
316+
return true, nil
229317
}
230318

231319
// localPushDevices pushes to the configured devices of a local user.

Diff for: pushserver/internal/pushrules/action.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package pushrules
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
)
8+
9+
type Action struct {
10+
Kind ActionKind `json:"-"` // Custom encoding in JSON.
11+
Tweak TweakKey `json:"-"` // Custom encoding in JSON.
12+
Value interface{} `json:"value,omitempty"`
13+
}
14+
15+
func (a *Action) MarshalJSON() ([]byte, error) {
16+
if a.Value == nil {
17+
return json.Marshal(a.Kind)
18+
}
19+
20+
if a.Kind != SetTweakAction {
21+
return nil, fmt.Errorf("only set_tweak actions may have a value, but got kind %q", a.Kind)
22+
}
23+
24+
return json.Marshal(map[string]interface{}{
25+
string(a.Kind): a.Tweak,
26+
"value": a.Value,
27+
})
28+
}
29+
30+
func (a *Action) UnmarshalJSON(bs []byte) error {
31+
if bytes.HasPrefix(bs, []byte("\"")) {
32+
return json.Unmarshal(bs, &a.Kind)
33+
}
34+
35+
var raw struct {
36+
SetTweak TweakKey `json:"set_tweak"`
37+
Value interface{} `json:"value"`
38+
}
39+
if err := json.Unmarshal(bs, &raw); err != nil {
40+
return err
41+
}
42+
if raw.SetTweak == UnknownTweak {
43+
return fmt.Errorf("got unknown action JSON: %s", string(bs))
44+
}
45+
a.Kind = SetTweakAction
46+
a.Tweak = raw.SetTweak
47+
a.Value = raw.Value
48+
49+
return nil
50+
}
51+
52+
type ActionKind string
53+
54+
const (
55+
UnknownAction ActionKind = ""
56+
NotifyAction ActionKind = "notify"
57+
DontNotifyAction ActionKind = "dont_notify"
58+
CoalesceAction ActionKind = "coalesce"
59+
SetTweakAction ActionKind = "set_tweak"
60+
)
61+
62+
type TweakKey string
63+
64+
const (
65+
UnknownTweak TweakKey = ""
66+
SoundTweak TweakKey = "sound"
67+
HighlightTweak TweakKey = "highlight"
68+
)

Diff for: pushserver/internal/pushrules/condition.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package pushrules
2+
3+
type Condition struct {
4+
Kind ConditionKind `json:"kind"` // Required.
5+
Key string `json:"key,omitempty"` // Required for EventMatchCondition and SenderNotificationPermissionCondition.
6+
Pattern string `json:"pattern,omitempty"` // Required for EventMatchCondition.
7+
Is string `json:"is,omitempty"` // Required for RoomMemberCountCondition.
8+
}
9+
10+
// ConditionKind represents a kind of condition.
11+
//
12+
// SPEC: Unrecognised conditions MUST NOT match any events,
13+
// effectively making the push rule disabled.
14+
type ConditionKind string
15+
16+
const (
17+
UnknownCondition ConditionKind = ""
18+
EventMatchCondition ConditionKind = "event_match"
19+
ContainsDisplayNameCondition ConditionKind = "contains_display_name"
20+
RoomMemberCountCondition ConditionKind = "room_member_count"
21+
SenderNotificationPermissionCondition ConditionKind = "sender_notification_permission"
22+
)

Diff for: pushserver/internal/pushrules/default.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package pushrules
2+
3+
import (
4+
"github.com/matrix-org/gomatrixserverlib"
5+
)
6+
7+
// DefaultRuleSet returns the default ruleset for a given (fully
8+
// qualified) MXID.
9+
func DefaultRuleSet(localpart string, serverName gomatrixserverlib.ServerName) *RuleSet {
10+
return &RuleSet{
11+
Override: defaultOverrideRules("@" + localpart + ":" + string(serverName)),
12+
Content: defaultContentRules(localpart),
13+
Underride: defaultUnderrideRules,
14+
}
15+
}

Diff for: pushserver/internal/pushrules/default_content.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package pushrules
2+
3+
func defaultContentRules(localpart string) []*Rule {
4+
return []*Rule{
5+
mRuleContainsUserNameDefinition(localpart),
6+
}
7+
}
8+
9+
const (
10+
MRuleContainsUserName = ".m.rule.contains_user_name"
11+
)
12+
13+
func mRuleContainsUserNameDefinition(localpart string) *Rule {
14+
return &Rule{
15+
RuleID: MRuleContainsUserName,
16+
Default: true,
17+
Enabled: true,
18+
Pattern: localpart,
19+
Actions: []*Action{
20+
{Kind: NotifyAction},
21+
{
22+
Kind: SetTweakAction,
23+
Tweak: SoundTweak,
24+
Value: "default",
25+
},
26+
{
27+
Kind: SetTweakAction,
28+
Tweak: SoundTweak,
29+
Value: "default",
30+
},
31+
},
32+
}
33+
}

0 commit comments

Comments
 (0)