diff options
author | Tulir Asokan <tulir@maunium.net> | 2018-04-15 15:36:01 +0300 |
---|---|---|
committer | Tulir Asokan <tulir@maunium.net> | 2018-04-15 15:36:01 +0300 |
commit | 0cdde557a3ed7624de31aa844929037b65e1fe11 (patch) | |
tree | 0d592f9e5d1071c00729dda03571731646242c31 | |
parent | bb9ed4558b6ee64d81a8092e1bb0304c75b49c22 (diff) |
Add tests for pushrule conditions and fix bugs found when making tests
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | coverage.html | 682 | ||||
-rw-r--r-- | matrix/pushrules/condition.go | 55 | ||||
-rw-r--r-- | matrix/pushrules/condition_displayname_test.go | 59 | ||||
-rw-r--r-- | matrix/pushrules/condition_eventmatch_test.go | 85 | ||||
-rw-r--r-- | matrix/pushrules/condition_membercount_test.go | 71 | ||||
-rw-r--r-- | matrix/pushrules/condition_test.go | 134 | ||||
-rw-r--r-- | matrix/pushrules/pushrules_test.go | 15 | ||||
-rw-r--r-- | matrix/pushrules/rule.go | 13 | ||||
-rw-r--r-- | matrix/pushrules/ruleset.go | 3 |
10 files changed, 1072 insertions, 46 deletions
@@ -1,3 +1,4 @@ .idea/ gomuks gomuks.exe +coverage.out diff --git a/coverage.html b/coverage.html new file mode 100644 index 0000000..c3d77dd --- /dev/null +++ b/coverage.html @@ -0,0 +1,682 @@ + +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + body { + background: black; + color: rgb(80, 80, 80); + } + body, pre, #legend span { + font-family: Menlo, monospace; + font-weight: bold; + } + #topbar { + background: black; + position: fixed; + top: 0; left: 0; right: 0; + height: 42px; + border-bottom: 1px solid rgb(80, 80, 80); + } + #content { + margin-top: 50px; + } + #nav, #legend { + float: left; + margin-left: 10px; + } + #legend { + margin-top: 12px; + } + #nav { + margin-top: 10px; + } + #legend span { + margin: 0 5px; + } + .cov0 { color: rgb(192, 0, 0) } +.cov1 { color: rgb(128, 128, 128) } +.cov2 { color: rgb(116, 140, 131) } +.cov3 { color: rgb(104, 152, 134) } +.cov4 { color: rgb(92, 164, 137) } +.cov5 { color: rgb(80, 176, 140) } +.cov6 { color: rgb(68, 188, 143) } +.cov7 { color: rgb(56, 200, 146) } +.cov8 { color: rgb(44, 212, 149) } +.cov9 { color: rgb(32, 224, 152) } +.cov10 { color: rgb(20, 236, 155) } + + </style> + </head> + <body> + <div id="topbar"> + <div id="nav"> + <select id="files"> + + <option value="file0">maunium.net/go/gomuks/matrix/pushrules/action.go (51.6%)</option> + + <option value="file1">maunium.net/go/gomuks/matrix/pushrules/condition.go (97.5%)</option> + + <option value="file2">maunium.net/go/gomuks/matrix/pushrules/pushrules.go (50.0%)</option> + + <option value="file3">maunium.net/go/gomuks/matrix/pushrules/rule.go (13.6%)</option> + + <option value="file4">maunium.net/go/gomuks/matrix/pushrules/ruleset.go (52.9%)</option> + + </select> + </div> + <div id="legend"> + <span>not tracked</span> + + <span class="cov0">not covered</span> + <span class="cov8">covered</span> + + </div> + </div> + <div id="content"> + + <pre class="file" id="file0" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import "encoding/json" + +// PushActionType is the type of a PushAction +type PushActionType string + +// The allowed push action types as specified in spec section 11.12.1.4.1. +const ( + ActionNotify PushActionType = "notify" + ActionDontNotify PushActionType = "dont_notify" + ActionCoalesce PushActionType = "coalesce" + ActionSetTweak PushActionType = "set_tweak" +) + +// PushActionTweak is the type of the tweak in SetTweak push actions. +type PushActionTweak string + +// The allowed tweak types as specified in spec section 11.12.1.4.1.1. +const ( + TweakSound PushActionTweak = "sound" + TweakHighlight PushActionTweak = "highlight" +) + +// PushActionArray is an array of PushActions. +type PushActionArray []*PushAction + +// PushActionArrayShould contains the important information parsed from a PushActionArray. +type PushActionArrayShould struct { + // Whether or not the array contained a Notify, DontNotify or Coalesce action type. + NotifySpecified bool + // Whether or not the event in question should trigger a notification. + Notify bool + // Whether or not the event in question should be highlighted. + Highlight bool + + // Whether or not the event in question should trigger a sound alert. + PlaySound bool + // The name of the sound to play if PlaySound is true. + SoundName string +} + +// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct. +func (actions PushActionArray) Should() (should PushActionArrayShould) <span class="cov8" title="1">{ + for _, action := range actions </span><span class="cov8" title="1">{ + switch action.Action </span>{ + case ActionNotify, ActionCoalesce:<span class="cov0" title="0"> + should.Notify = true + should.NotifySpecified = true</span> + case ActionDontNotify:<span class="cov8" title="1"> + should.Notify = false + should.NotifySpecified = true</span> + case ActionSetTweak:<span class="cov0" title="0"> + switch action.Tweak </span>{ + case TweakHighlight:<span class="cov0" title="0"> + var ok bool + should.Highlight, ok = action.Value.(bool) + if !ok </span><span class="cov0" title="0">{ + // Highlight value not specified, so assume true since the tweak is set. + should.Highlight = true + }</span> + case TweakSound:<span class="cov0" title="0"> + should.SoundName = action.Value.(string) + should.PlaySound = len(should.SoundName) > 0</span> + } + } + } + <span class="cov8" title="1">return</span> +} + +// PushAction is a single action that should be triggered when receiving a message. +type PushAction struct { + Action PushActionType + Tweak PushActionTweak + Value interface{} +} + +// UnmarshalJSON parses JSON into this PushAction. +// +// * If the JSON is a single string, the value is stored in the Action field. +// * If the JSON is an object with the set_tweak field, Action will be set to +// "set_tweak", Tweak will be set to the value of the set_tweak field and +// and Value will be set to the value of the value field. +// * In any other case, the function does nothing. +func (action *PushAction) UnmarshalJSON(raw []byte) error <span class="cov8" title="1">{ + var data interface{} + + err := json.Unmarshal(raw, &data) + if err != nil </span><span class="cov0" title="0">{ + return err + }</span> + + <span class="cov8" title="1">switch val := data.(type) </span>{ + case string:<span class="cov8" title="1"> + action.Action = PushActionType(val)</span> + case map[string]interface{}:<span class="cov8" title="1"> + tweak, ok := val["set_tweak"].(string) + if ok </span><span class="cov8" title="1">{ + action.Action = ActionSetTweak + action.Tweak = PushActionTweak(tweak) + action.Value, _ = val["value"] + }</span> + } + <span class="cov8" title="1">return nil</span> +} + +// MarshalJSON is the reverse of UnmarshalJSON() +func (action *PushAction) MarshalJSON() (raw []byte, err error) <span class="cov0" title="0">{ + if action.Action == ActionSetTweak </span><span class="cov0" title="0">{ + data := map[string]interface{}{ + "set_tweak": action.Tweak, + "value": action.Value, + } + return json.Marshal(&data) + }</span><span class="cov0" title="0"> else { + data := string(action.Action) + return json.Marshal(&data) + }</span> +} +</pre> + + <pre class="file" id="file1" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import ( + "regexp" + "strconv" + "strings" + + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/rooms" +) + +// Room is an interface with the functions that are needed for processing room-specific push conditions +type Room interface { + GetMember(mxid string) *rooms.Member + GetMembers() map[string]*rooms.Member + GetSessionOwner() *rooms.Member +} + +// PushCondKind is the type of a push condition. +type PushCondKind string + +// The allowed push condition kinds as specified in section 11.12.1.4.3 of r0.3.0 of the Client-Server API. +const ( + KindEventMatch PushCondKind = "event_match" + KindContainsDisplayName PushCondKind = "contains_display_name" + KindRoomMemberCount PushCondKind = "room_member_count" +) + +// PushCondition wraps a condition that is required for a specific PushRule to be used. +type PushCondition struct { + // The type of the condition. + Kind PushCondKind `json:"kind"` + // The dot-separated field of the event to match. Only applicable if kind is EventMatch. + Key string `json:"key,omitempty"` + // The glob-style pattern to match the field against. Only applicable if kind is EventMatch. + Pattern string `json:"pattern,omitempty"` + // The condition that needs to be fulfilled for RoomMemberCount-type conditions. + // A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found. + MemberCountCondition string `json:"is,omitempty"` +} + +// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions. +var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") + +// Match checks if this condition is fulfilled for the given event in the given room. +func (cond *PushCondition) Match(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + switch cond.Kind </span>{ + case KindEventMatch:<span class="cov8" title="1"> + return cond.matchValue(room, event)</span> + case KindContainsDisplayName:<span class="cov8" title="1"> + return cond.matchDisplayName(room, event)</span> + case KindRoomMemberCount:<span class="cov8" title="1"> + return cond.matchMemberCount(room, event)</span> + default:<span class="cov8" title="1"> + return false</span> + } +} + +func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + index := strings.IndexRune(cond.Key, '.') + key := cond.Key + subkey := "" + if index > 0 </span><span class="cov8" title="1">{ + subkey = key[index+1:] + key = key[0:index] + }</span> + + <span class="cov8" title="1">pattern, _ := glob.Compile(cond.Pattern) + + switch key </span>{ + case "type":<span class="cov8" title="1"> + return pattern.MatchString(event.Type)</span> + case "sender":<span class="cov8" title="1"> + return pattern.MatchString(event.Sender)</span> + case "room_id":<span class="cov8" title="1"> + return pattern.MatchString(event.RoomID)</span> + case "state_key":<span class="cov8" title="1"> + if event.StateKey == nil </span><span class="cov8" title="1">{ + return cond.Pattern == "" + }</span> + <span class="cov8" title="1">return pattern.MatchString(*event.StateKey)</span> + case "content":<span class="cov8" title="1"> + val, _ := event.Content[subkey].(string) + return pattern.MatchString(val)</span> + default:<span class="cov8" title="1"> + return false</span> + } +} + +func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + member := room.GetSessionOwner() + if member == nil || member.UserID == event.Sender </span><span class="cov8" title="1">{ + return false + }</span> + <span class="cov8" title="1">text, _ := event.Content["body"].(string) + return strings.Contains(text, member.DisplayName)</span> +} + +func (cond *PushCondition) matchMemberCount(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) + if len(group) != 3 </span><span class="cov8" title="1">{ + return false + }</span> + + <span class="cov8" title="1">operator := group[1] + wantedMemberCount, _ := strconv.Atoi(group[2]) + + memberCount := len(room.GetMembers()) + + switch operator </span>{ + case "==", "":<span class="cov8" title="1"> + return memberCount == wantedMemberCount</span> + case ">":<span class="cov8" title="1"> + return memberCount > wantedMemberCount</span> + case ">=":<span class="cov8" title="1"> + return memberCount >= wantedMemberCount</span> + case "<":<span class="cov8" title="1"> + return memberCount < wantedMemberCount</span> + case "<=":<span class="cov8" title="1"> + return memberCount <= wantedMemberCount</span> + default:<span class="cov0" title="0"> + // Should be impossible due to regex. + return false</span> + } +} +</pre> + + <pre class="file" id="file2" style="display: none">package pushrules + +import ( + "encoding/json" + "net/url" + + "maunium.net/go/gomatrix" +) + +// GetPushRules returns the push notification rules for the global scope. +func GetPushRules(client *gomatrix.Client) (*PushRuleset, error) <span class="cov0" title="0">{ + return GetScopedPushRules(client, "global") +}</span> + +// GetScopedPushRules returns the push notification rules for the given scope. +func GetScopedPushRules(client *gomatrix.Client, scope string) (resp *PushRuleset, err error) <span class="cov0" title="0">{ + u, _ := url.Parse(client.BuildURL("pushrules", scope)) + // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. + u.Path += "/" + _, err = client.MakeRequest("GET", u.String(), nil, &resp) + return +}</span> + +// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. +func EventToPushRules(event *gomatrix.Event) (*PushRuleset, error) <span class="cov8" title="1">{ + content, _ := event.Content["global"] + raw, err := json.Marshal(content) + if err != nil </span><span class="cov0" title="0">{ + return nil, err + }</span> + + <span class="cov8" title="1">ruleset := &PushRuleset{} + err = json.Unmarshal(raw, ruleset) + if err != nil </span><span class="cov0" title="0">{ + return nil, err + }</span> + + <span class="cov8" title="1">return ruleset, nil</span> +} +</pre> + + <pre class="file" id="file3" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import ( + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" +) + +type PushRuleCollection interface { + GetActions(room Room, event *gomatrix.Event) PushActionArray +} + +type PushRuleArray []*PushRule + +func (rules PushRuleArray) setType(typ PushRuleType) PushRuleArray <span class="cov8" title="1">{ + for _, rule := range rules </span><span class="cov8" title="1">{ + rule.Type = typ + }</span> + <span class="cov8" title="1">return rules</span> +} + +func (rules PushRuleArray) GetActions(room Room, event *gomatrix.Event) PushActionArray <span class="cov0" title="0">{ + for _, rule := range rules </span><span class="cov0" title="0">{ + if !rule.Match(room, event) </span><span class="cov0" title="0">{ + continue</span> + } + <span class="cov0" title="0">return rule.Actions</span> + } + <span class="cov0" title="0">return nil</span> +} + +type PushRuleMap struct { + Map map[string]*PushRule + Type PushRuleType +} + +func (rules PushRuleArray) setTypeAndMap(typ PushRuleType) PushRuleMap <span class="cov8" title="1">{ + data := PushRuleMap{ + Map: make(map[string]*PushRule), + Type: typ, + } + for _, rule := range rules </span><span class="cov0" title="0">{ + rule.Type = typ + data.Map[rule.RuleID] = rule + }</span> + <span class="cov8" title="1">return data</span> +} + +func (ruleMap PushRuleMap) GetActions(room Room, event *gomatrix.Event) PushActionArray <span class="cov0" title="0">{ + var rule *PushRule + var found bool + switch ruleMap.Type </span>{ + case RoomRule:<span class="cov0" title="0"> + rule, found = ruleMap.Map[event.RoomID]</span> + case SenderRule:<span class="cov0" title="0"> + rule, found = ruleMap.Map[event.Sender]</span> + } + <span class="cov0" title="0">if found && rule.Match(room, event) </span><span class="cov0" title="0">{ + return rule.Actions + }</span> + <span class="cov0" title="0">return nil</span> +} + +func (ruleMap PushRuleMap) unmap() PushRuleArray <span class="cov0" title="0">{ + array := make(PushRuleArray, len(ruleMap.Map)) + index := 0 + for _, rule := range ruleMap.Map </span><span class="cov0" title="0">{ + array[index] = rule + index++ + }</span> + <span class="cov0" title="0">return array</span> +} + +type PushRuleType string + +const ( + OverrideRule PushRuleType = "override" + ContentRule PushRuleType = "content" + RoomRule PushRuleType = "room" + SenderRule PushRuleType = "sender" + UnderrideRule PushRuleType = "underride" +) + +type PushRule struct { + // The type of this rule. + Type PushRuleType `json:"-"` + // The ID of this rule. + // For room-specific rules and user-specific rules, this is the room or user ID (respectively) + // For other types of rules, this doesn't affect anything. + RuleID string `json:"rule_id"` + // The actions this rule should trigger when matched. + Actions PushActionArray `json:"actions"` + // Whether this is a default rule, or has been set explicitly. + Default bool `json:"default"` + // Whether or not this push rule is enabled. + Enabled bool `json:"enabled"` + // The conditions to match in order to trigger this rule. + // Only applicable to generic underride/override rules. + Conditions []*PushCondition `json:"conditions,omitempty"` + // Pattern for content-specific push rules + Pattern string `json:"pattern,omitempty"` +} + +func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool <span class="cov0" title="0">{ + if !rule.Enabled </span><span class="cov0" title="0">{ + return false + }</span> + <span class="cov0" title="0">switch rule.Type </span>{ + case OverrideRule, UnderrideRule:<span class="cov0" title="0"> + return rule.matchConditions(room, event)</span> + case ContentRule:<span class="cov0" title="0"> + return rule.matchPattern(room, event)</span> + case RoomRule:<span class="cov0" title="0"> + return rule.RuleID == event.RoomID</span> + case SenderRule:<span class="cov0" title="0"> + return rule.RuleID == event.Sender</span> + default:<span class="cov0" title="0"> + return false</span> + } +} + +func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool <span class="cov0" title="0">{ + for _, cond := range rule.Conditions </span><span class="cov0" title="0">{ + if !cond.Match(room, event) </span><span class="cov0" title="0">{ + return false + }</span> + } + <span class="cov0" title="0">return true</span> +} + +func (rule *PushRule) matchPattern(room Room, event *gomatrix.Event) bool <span class="cov0" title="0">{ + pattern, err := glob.Compile(rule.Pattern) + if err != nil </span><span class="cov0" title="0">{ + return false + }</span> + <span class="cov0" title="0">text, _ := event.Content["body"].(string) + return pattern.MatchString(text)</span> +} +</pre> + + <pre class="file" id="file4" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import ( + "encoding/json" + + "maunium.net/go/gomatrix" +) + +type PushRuleset struct { + Override PushRuleArray + Content PushRuleArray + Room PushRuleMap + Sender PushRuleMap + Underride PushRuleArray +} + +type rawPushRuleset struct { + Override PushRuleArray `json:"override"` + Content PushRuleArray `json:"content"` + Room PushRuleArray `json:"room"` + Sender PushRuleArray `json:"sender"` + Underride PushRuleArray `json:"underride"` +} + +// UnmarshalJSON parses JSON into this PushRuleset. +// +// For override, sender and underride push rule arrays, the type is added +// to each PushRule and the array is used as-is. +// +// For room and sender push rule arrays, the type is added to each PushRule +// and the array is converted to a map with the rule ID as the key and the +// PushRule as the value. +func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) <span class="cov8" title="1">{ + data := rawPushRuleset{} + err = json.Unmarshal(raw, &data) + if err != nil </span><span class="cov0" title="0">{ + return + }</span> + + <span class="cov8" title="1">rs.Override = data.Override.setType(OverrideRule) + rs.Content = data.Content.setType(ContentRule) + rs.Room = data.Room.setTypeAndMap(RoomRule) + rs.Sender = data.Sender.setTypeAndMap(SenderRule) + rs.Underride = data.Underride.setType(UnderrideRule) + return</span> +} + +// MarshalJSON is the reverse of UnmarshalJSON() +func (rs *PushRuleset) MarshalJSON() ([]byte, error) <span class="cov0" title="0">{ + data := rawPushRuleset{ + Override: rs.Override, + Content: rs.Content, + Room: rs.Room.unmap(), + Sender: rs.Sender.unmap(), + Underride: rs.Underride, + } + return json.Marshal(&data) +}</span> + +// DefaultPushActions is the value returned if none of the rule +// collections in a Ruleset match the event given to GetActions() +var DefaultPushActions = make(PushActionArray, 0) + +// GetActions matches the given event against all of the push rule +// collections in this push ruleset in the order of priority as +// specified in spec section 11.12.1.4. +func (rs *PushRuleset) GetActions(room Room, event *gomatrix.Event) (match PushActionArray) <span class="cov0" title="0">{ + // Add push rule collections to array in priority order + arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} + // Loop until one of the push rule collections matches the room/event combo. + for _, pra := range arrays </span><span class="cov0" title="0">{ + if match = pra.GetActions(room, event); match != nil </span><span class="cov0" title="0">{ + // Match found, return it. + return + }</span> + } + // No match found, return default actions. + <span class="cov0" title="0">return DefaultPushActions</span> +} +</pre> + + </div> + </body> + <script> + (function() { + var files = document.getElementById('files'); + var visible; + files.addEventListener('change', onChange, false); + function select(part) { + if (visible) + visible.style.display = 'none'; + visible = document.getElementById(part); + if (!visible) + return; + files.value = part; + visible.style.display = 'block'; + location.hash = part; + } + function onChange() { + select(files.value); + window.scrollTo(0, 0); + } + if (location.hash != "") { + select(location.hash.substr(1)); + } + if (!visible) { + select("file0"); + } + })(); + </script> +</html> diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go index e9b11af..4d17695 100644 --- a/matrix/pushrules/condition.go +++ b/matrix/pushrules/condition.go @@ -26,6 +26,13 @@ import ( "maunium.net/go/gomuks/matrix/rooms" ) +// Room is an interface with the functions that are needed for processing room-specific push conditions +type Room interface { + GetMember(mxid string) *rooms.Member + GetMembers() map[string]*rooms.Member + GetSessionOwner() *rooms.Member +} + // PushCondKind is the type of a push condition. type PushCondKind string @@ -53,7 +60,7 @@ type PushCondition struct { var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") // Match checks if this condition is fulfilled for the given event in the given room. -func (cond *PushCondition) Match(room *rooms.Room, event *gomatrix.Event) bool { +func (cond *PushCondition) Match(room Room, event *gomatrix.Event) bool { switch cond.Kind { case KindEventMatch: return cond.matchValue(room, event) @@ -66,7 +73,7 @@ func (cond *PushCondition) Match(room *rooms.Room, event *gomatrix.Event) bool { } } -func (cond *PushCondition) matchValue(room *rooms.Room, event *gomatrix.Event) bool { +func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool { index := strings.IndexRune(cond.Key, '.') key := cond.Key subkey := "" @@ -75,10 +82,7 @@ func (cond *PushCondition) matchValue(room *rooms.Room, event *gomatrix.Event) b key = key[0:index] } - pattern, err := glob.Compile(cond.Pattern) - if err != nil { - return false - } + pattern, _ := glob.Compile(cond.Pattern) switch key { case "type": @@ -100,48 +104,39 @@ func (cond *PushCondition) matchValue(room *rooms.Room, event *gomatrix.Event) b } } -func (cond *PushCondition) matchDisplayName(room *rooms.Room, event *gomatrix.Event) bool { - member := room.GetMember(room.SessionUserID) - if member == nil { +func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bool { + member := room.GetSessionOwner() + if member == nil || member.UserID == event.Sender { return false } text, _ := event.Content["body"].(string) return strings.Contains(text, member.DisplayName) } -func (cond *PushCondition) matchMemberCount(room *rooms.Room, event *gomatrix.Event) bool { - groupGroups := MemberCountFilterRegex.FindAllStringSubmatch(cond.MemberCountCondition, -1) - if len(groupGroups) != 1 { +func (cond *PushCondition) matchMemberCount(room Room, event *gomatrix.Event) bool { + group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) + if len(group) != 3 { return false } - operator := "==" - wantedMemberCount := 0 - - group := groupGroups[0] - if len(group) == 0 { - return false - } else if len(group) == 1 { - wantedMemberCount, _ = strconv.Atoi(group[0]) - } else { - operator = group[0] - wantedMemberCount, _ = strconv.Atoi(group[1]) - } + operator := group[1] + wantedMemberCount, _ := strconv.Atoi(group[2]) memberCount := len(room.GetMembers()) switch operator { - case "==": - return wantedMemberCount == memberCount + case "==", "": + return memberCount == wantedMemberCount case ">": - return wantedMemberCount > memberCount + return memberCount > wantedMemberCount case ">=": - return wantedMemberCount >= memberCount + return memberCount >= wantedMemberCount case "<": - return wantedMemberCount < memberCount + return memberCount < wantedMemberCount case "<=": - return wantedMemberCount <= memberCount + return memberCount <= wantedMemberCount default: + // Should be impossible due to regex. return false } } diff --git a/matrix/pushrules/condition_displayname_test.go b/matrix/pushrules/condition_displayname_test.go new file mode 100644 index 0000000..e859ff8 --- /dev/null +++ b/matrix/pushrules/condition_displayname_test.go @@ -0,0 +1,59 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPushCondition_Match_DisplayName(t *testing.T) { + event := newFakeEvent("m.room.message", map[string]interface{}{ + "msgtype": "m.text", + "body": "tulir: test mention", + }) + event.Sender = "@someone_else:matrix.org" + assert.True(t, displaynamePushCondition.Match(displaynameTestRoom, event)) +} + +func TestPushCondition_Match_DisplayName_Fail(t *testing.T) { + event := newFakeEvent("m.room.message", map[string]interface{}{ + "msgtype": "m.text", + "body": "not a mention", + }) + event.Sender = "@someone_else:matrix.org" + assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event)) +} + +func TestPushCondition_Match_DisplayName_CantHighlightSelf(t *testing.T) { + event := newFakeEvent("m.room.message", map[string]interface{}{ + "msgtype": "m.text", + "body": "tulir: I can't highlight myself", + }) + assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event)) +} + +func TestPushCondition_Match_DisplayName_FailsOnEmptyRoom(t *testing.T) { + emptyRoom := newFakeRoom(0) + event := newFakeEvent("m.room.message", map[string]interface{}{ + "msgtype": "m.text", + "body": "tulir: this room doesn't have the owner Member available, so it fails.", + }) + event.Sender = "@someone_else:matrix.org" + assert.False(t, displaynamePushCondition.Match(emptyRoom, event)) +} diff --git a/matrix/pushrules/condition_eventmatch_test.go b/matrix/pushrules/condition_eventmatch_test.go new file mode 100644 index 0000000..2fcd054 --- /dev/null +++ b/matrix/pushrules/condition_eventmatch_test.go @@ -0,0 +1,85 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPushCondition_Match_KindEvent_MsgType(t *testing.T) { + condition := newMatchPushCondition("content.msgtype", "m.emote") + event := newFakeEvent("m.room.message", map[string]interface{}{ + "msgtype": "m.emote", + "body": "tests gomuks pushconditions", + }) + assert.True(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_MsgType_Fail(t *testing.T) { + condition := newMatchPushCondition("content.msgtype", "m.emote") + + event := newFakeEvent("m.room.message", map[string]interface{}{ + "msgtype": "m.text", + "body": "I'm testing gomuks pushconditions", + }) + assert.False(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_EventType(t *testing.T) { + condition := newMatchPushCondition("type", "m.room.foo") + event := newFakeEvent("m.room.foo", map[string]interface{}{}) + assert.True(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_Sender_Fail(t *testing.T) { + condition := newMatchPushCondition("sender", "@foo:maunium.net") + event := newFakeEvent("m.room.foo", map[string]interface{}{}) + assert.False(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_RoomID(t *testing.T) { + condition := newMatchPushCondition("room_id", "!fakeroom:maunium.net") + event := newFakeEvent("", map[string]interface{}{}) + assert.True(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_BlankStateKey(t *testing.T) { + condition := newMatchPushCondition("state_key", "") + event := newFakeEvent("m.room.foo", map[string]interface{}{}) + assert.True(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_BlankStateKey_Fail(t *testing.T) { + condition := newMatchPushCondition("state_key", "not blank") + event := newFakeEvent("m.room.foo", map[string]interface{}{}) + assert.False(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_NonBlankStateKey(t *testing.T) { + condition := newMatchPushCondition("state_key", "*:maunium.net") + event := newFakeEvent("m.room.foo", map[string]interface{}{}) + event.StateKey = &event.Sender + assert.True(t, condition.Match(blankTestRoom, event)) +} + +func TestPushCondition_Match_KindEvent_UnknownKey(t *testing.T) { + condition := newMatchPushCondition("non-existent key", "doesn't affect anything") + event := newFakeEvent("m.room.foo", map[string]interface{}{}) + assert.False(t, condition.Match(blankTestRoom, event)) +} diff --git a/matrix/pushrules/condition_membercount_test.go b/matrix/pushrules/condition_membercount_test.go new file mode 100644 index 0000000..32a776b --- /dev/null +++ b/matrix/pushrules/condition_membercount_test.go @@ -0,0 +1,71 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPushCondition_Match_KindMemberCount_OneToOne_ImplicitPrefix(t *testing.T) { + condition := newCountPushCondition("2") + room := newFakeRoom(2) + assert.True(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_OneToOne_ExplicitPrefix(t *testing.T) { + condition := newCountPushCondition("==2") + room := newFakeRoom(2) + assert.True(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_BigRoom(t *testing.T) { + condition := newCountPushCondition(">200") + room := newFakeRoom(201) + assert.True(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_BigRoom_Fail(t *testing.T) { + condition := newCountPushCondition(">=200") + room := newFakeRoom(199) + assert.False(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_SmallRoom(t *testing.T) { + condition := newCountPushCondition("<10") + room := newFakeRoom(9) + assert.True(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_SmallRoom_Fail(t *testing.T) { + condition := newCountPushCondition("<=10") + room := newFakeRoom(11) + assert.False(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_InvalidPrefix(t *testing.T) { + condition := newCountPushCondition("??10") + room := newFakeRoom(11) + assert.False(t, condition.Match(room, countConditionTestEvent)) +} + +func TestPushCondition_Match_KindMemberCount_InvalidCondition(t *testing.T) { + condition := newCountPushCondition("foobar") + room := newFakeRoom(1) + assert.False(t, condition.Match(room, countConditionTestEvent)) +} diff --git a/matrix/pushrules/condition_test.go b/matrix/pushrules/condition_test.go new file mode 100644 index 0000000..7fd06ee --- /dev/null +++ b/matrix/pushrules/condition_test.go @@ -0,0 +1,134 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/pushrules" + "maunium.net/go/gomuks/matrix/rooms" +) + +var ( + blankTestRoom *rooms.Room + displaynameTestRoom pushrules.Room + + countConditionTestEvent *gomatrix.Event + + displaynamePushCondition *pushrules.PushCondition +) + +func init() { + blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net") + + countConditionTestEvent = &gomatrix.Event{ + Sender: "@tulir:maunium.net", + Type: "m.room.message", + Timestamp: 1523791120, + ID: "$123:maunium.net", + RoomID: "!fakeroom:maunium.net", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "test", + }, + } + + displaynameTestRoom = newFakeRoom(4) + displaynamePushCondition = &pushrules.PushCondition{ + Kind: pushrules.KindContainsDisplayName, + } +} + +func newFakeEvent(evtType string, content map[string]interface{}) *gomatrix.Event { + return &gomatrix.Event{ + Sender: "@tulir:maunium.net", + Type: evtType, + Timestamp: 1523791120, + ID: "$123:maunium.net", + RoomID: "!fakeroom:maunium.net", + Content: content, + } +} + +func newCountPushCondition(condition string) *pushrules.PushCondition { + return &pushrules.PushCondition{ + Kind: pushrules.KindRoomMemberCount, + MemberCountCondition: condition, + } +} + +func newMatchPushCondition(key, pattern string) *pushrules.PushCondition { + return &pushrules.PushCondition{ + Kind: pushrules.KindEventMatch, + Key: key, + Pattern: pattern, + } +} + +func TestPushCondition_Match_InvalidKind(t *testing.T) { + condition := &pushrules.PushCondition{ + Kind: pushrules.PushCondKind("invalid"), + } + event := newFakeEvent("m.room.foobar", map[string]interface{}{}) + assert.False(t, condition.Match(blankTestRoom, event)) +} + +type FakeRoom struct { + members map[string]*rooms.Member + owner string +} + +func newFakeRoom(memberCount int) *FakeRoom { + room := &FakeRoom{ + owner: "@tulir:maunium.net", + members: make(map[string]*rooms.Member), + } + + if memberCount >= 1 { + room.members["@tulir:maunium.net"] = &rooms.Member{ + UserID: "@tulir:maunium.net", + Membership: rooms.MembershipJoin, + DisplayName: "tulir", + } + } + + for i := 0; i < memberCount-1; i++ { + mxid := fmt.Sprintf("@extrauser_%d:matrix.org", i) + room.members[mxid] = &rooms.Member{ + UserID: mxid, + Membership: rooms.MembershipJoin, + DisplayName: fmt.Sprintf("Extra User %d", i), + } + } + + return room +} + +func (fr *FakeRoom) GetMember(mxid string) *rooms.Member { + return fr.members[mxid] +} + +func (fr *FakeRoom) GetSessionOwner() *rooms.Member { + return fr.members[fr.owner] +} + +func (fr *FakeRoom) GetMembers() map[string]*rooms.Member { + return fr.members +} diff --git a/matrix/pushrules/pushrules_test.go b/matrix/pushrules/pushrules_test.go index 7e0e72e..09698ac 100644 --- a/matrix/pushrules/pushrules_test.go +++ b/matrix/pushrules/pushrules_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. -package pushrules +package pushrules_test import ( "encoding/json" @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/pushrules" ) var mapExamplePushRules map[string]interface{} @@ -37,15 +38,15 @@ func TestEventToPushRules(t *testing.T) { Timestamp: 1523380910, Content: mapExamplePushRules, } - pushRuleset, err := EventToPushRules(event) + pushRuleset, err := pushrules.EventToPushRules(event) assert.Nil(t, err) assert.NotNil(t, pushRuleset) - assert.IsType(t, pushRuleset.Override, PushRuleArray{}) - assert.IsType(t, pushRuleset.Content, PushRuleArray{}) - assert.IsType(t, pushRuleset.Room, PushRuleMap{}) - assert.IsType(t, pushRuleset.Sender, PushRuleMap{}) - assert.IsType(t, pushRuleset.Underride, PushRuleArray{}) + assert.IsType(t, pushRuleset.Override, pushrules.PushRuleArray{}) + assert.IsType(t, pushRuleset.Content, pushrules.PushRuleArray{}) + assert.IsType(t, pushRuleset.Room, pushrules.PushRuleMap{}) + assert.IsType(t, pushRuleset.Sender, pushrules.PushRuleMap{}) + assert.IsType(t, pushRuleset.Underride, pushrules.PushRuleArray{}) assert.Len(t, pushRuleset.Override, 2) assert.Len(t, pushRuleset.Content, 1) assert.Empty(t, pushRuleset.Room.Map) diff --git a/matrix/pushrules/rule.go b/matrix/pushrules/rule.go index dd8a4d3..0caa13d 100644 --- a/matrix/pushrules/rule.go +++ b/matrix/pushrules/rule.go @@ -19,11 +19,10 @@ package pushrules import ( "github.com/zyedidia/glob" "maunium.net/go/gomatrix" - "maunium.net/go/gomuks/matrix/rooms" ) type PushRuleCollection interface { - GetActions(room *rooms.Room, event *gomatrix.Event) PushActionArray + GetActions(room Room, event *gomatrix.Event) PushActionArray } type PushRuleArray []*PushRule @@ -35,7 +34,7 @@ func (rules PushRuleArray) setType(typ PushRuleType) PushRuleArray { return rules } -func (rules PushRuleArray) GetActions(room *rooms.Room, event *gomatrix.Event) PushActionArray { +func (rules PushRuleArray) GetActions(room Room, event *gomatrix.Event) PushActionArray { for _, rule := range rules { if !rule.Match(room, event) { continue @@ -62,7 +61,7 @@ func (rules PushRuleArray) setTypeAndMap(typ PushRuleType) PushRuleMap { return data } -func (ruleMap PushRuleMap) GetActions(room *rooms.Room, event *gomatrix.Event) PushActionArray { +func (ruleMap PushRuleMap) GetActions(room Room, event *gomatrix.Event) PushActionArray { var rule *PushRule var found bool switch ruleMap.Type { @@ -117,7 +116,7 @@ type PushRule struct { Pattern string `json:"pattern,omitempty"` } -func (rule *PushRule) Match(room *rooms.Room, event *gomatrix.Event) bool { +func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool { if !rule.Enabled { return false } @@ -135,7 +134,7 @@ func (rule *PushRule) Match(room *rooms.Room, event *gomatrix.Event) bool { } } -func (rule *PushRule) matchConditions(room *rooms.Room, event *gomatrix.Event) bool { +func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool { for _, cond := range rule.Conditions { if !cond.Match(room, event) { return false @@ -144,7 +143,7 @@ func (rule *PushRule) matchConditions(room *rooms.Room, event *gomatrix.Event) b return true } -func (rule *PushRule) matchPattern(room *rooms.Room, event *gomatrix.Event) bool { +func (rule *PushRule) matchPattern(room Room, event *gomatrix.Event) bool { pattern, err := glob.Compile(rule.Pattern) if err != nil { return false diff --git a/matrix/pushrules/ruleset.go b/matrix/pushrules/ruleset.go index f3026c8..8402dc9 100644 --- a/matrix/pushrules/ruleset.go +++ b/matrix/pushrules/ruleset.go @@ -20,7 +20,6 @@ import ( "encoding/json" "maunium.net/go/gomatrix" - "maunium.net/go/gomuks/matrix/rooms" ) type PushRuleset struct { @@ -81,7 +80,7 @@ var DefaultPushActions = make(PushActionArray, 0) // GetActions matches the given event against all of the push rule // collections in this push ruleset in the order of priority as // specified in spec section 11.12.1.4. -func (rs *PushRuleset) GetActions(room *rooms.Room, event *gomatrix.Event) (match PushActionArray) { +func (rs *PushRuleset) GetActions(room Room, event *gomatrix.Event) (match PushActionArray) { // Add push rule collections to array in priority order arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} // Loop until one of the push rule collections matches the room/event combo. |