// 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) { for _, action := range actions { switch action.Action { case ActionNotify, ActionCoalesce: should.Notify = true should.NotifySpecified = true case ActionDontNotify: should.Notify = false should.NotifySpecified = true case ActionSetTweak: switch action.Tweak { case TweakHighlight: var ok bool should.Highlight, ok = action.Value.(bool) if !ok { // Highlight value not specified, so assume true since the tweak is set. should.Highlight = true } case TweakSound: should.SoundName = action.Value.(string) should.PlaySound = len(should.SoundName) > 0 } } } return } // 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 { var data interface{} err := json.Unmarshal(raw, &data) if err != nil { return err } switch val := data.(type) { case string: action.Action = PushActionType(val) case map[string]interface{}: tweak, ok := val["set_tweak"].(string) if ok { action.Action = ActionSetTweak action.Tweak = PushActionTweak(tweak) action.Value, _ = val["value"] } } return nil } // MarshalJSON is the reverse of UnmarshalJSON() func (action *PushAction) MarshalJSON() (raw []byte, err error) { if action.Action == ActionSetTweak { data := map[string]interface{}{ "set_tweak": action.Tweak, "value": action.Value, } return json.Marshal(&data) } else { data := string(action.Action) return json.Marshal(&data) } }
// 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 { switch cond.Kind { case KindEventMatch: return cond.matchValue(room, event) case KindContainsDisplayName: return cond.matchDisplayName(room, event) case KindRoomMemberCount: return cond.matchMemberCount(room, event) default: return false } } func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool { index := strings.IndexRune(cond.Key, '.') key := cond.Key subkey := "" if index > 0 { subkey = key[index+1:] key = key[0:index] } pattern, _ := glob.Compile(cond.Pattern) switch key { case "type": return pattern.MatchString(event.Type) case "sender": return pattern.MatchString(event.Sender) case "room_id": return pattern.MatchString(event.RoomID) case "state_key": if event.StateKey == nil { return cond.Pattern == "" } return pattern.MatchString(*event.StateKey) case "content": val, _ := event.Content[subkey].(string) return pattern.MatchString(val) default: return false } } 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 Room, event *gomatrix.Event) bool { group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) if len(group) != 3 { return false } operator := group[1] wantedMemberCount, _ := strconv.Atoi(group[2]) memberCount := len(room.GetMembers()) switch operator { case "==", "": return memberCount == wantedMemberCount case ">": return memberCount > wantedMemberCount case ">=": return memberCount >= wantedMemberCount case "<": return memberCount < wantedMemberCount case "<=": return memberCount <= wantedMemberCount default: // Should be impossible due to regex. return false } }
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) { return GetScopedPushRules(client, "global") } // GetScopedPushRules returns the push notification rules for the given scope. func GetScopedPushRules(client *gomatrix.Client, scope string) (resp *PushRuleset, err error) { 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 } // EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. func EventToPushRules(event *gomatrix.Event) (*PushRuleset, error) { content, _ := event.Content["global"] raw, err := json.Marshal(content) if err != nil { return nil, err } ruleset := &PushRuleset{} err = json.Unmarshal(raw, ruleset) if err != nil { return nil, err } return ruleset, nil }
// 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 { for _, rule := range rules { rule.Type = typ } return rules } func (rules PushRuleArray) GetActions(room Room, event *gomatrix.Event) PushActionArray { for _, rule := range rules { if !rule.Match(room, event) { continue } return rule.Actions } return nil } type PushRuleMap struct { Map map[string]*PushRule Type PushRuleType } func (rules PushRuleArray) setTypeAndMap(typ PushRuleType) PushRuleMap { data := PushRuleMap{ Map: make(map[string]*PushRule), Type: typ, } for _, rule := range rules { rule.Type = typ data.Map[rule.RuleID] = rule } return data } func (ruleMap PushRuleMap) GetActions(room Room, event *gomatrix.Event) PushActionArray { var rule *PushRule var found bool switch ruleMap.Type { case RoomRule: rule, found = ruleMap.Map[event.RoomID] case SenderRule: rule, found = ruleMap.Map[event.Sender] } if found && rule.Match(room, event) { return rule.Actions } return nil } func (ruleMap PushRuleMap) unmap() PushRuleArray { array := make(PushRuleArray, len(ruleMap.Map)) index := 0 for _, rule := range ruleMap.Map { array[index] = rule index++ } return array } 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 { if !rule.Enabled { return false } switch rule.Type { case OverrideRule, UnderrideRule: return rule.matchConditions(room, event) case ContentRule: return rule.matchPattern(room, event) case RoomRule: return rule.RuleID == event.RoomID case SenderRule: return rule.RuleID == event.Sender default: return false } } func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool { for _, cond := range rule.Conditions { if !cond.Match(room, event) { return false } } return true } func (rule *PushRule) matchPattern(room Room, event *gomatrix.Event) bool { pattern, err := glob.Compile(rule.Pattern) if err != nil { return false } text, _ := event.Content["body"].(string) return pattern.MatchString(text) }
// 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) { data := rawPushRuleset{} err = json.Unmarshal(raw, &data) if err != nil { return } 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 } // MarshalJSON is the reverse of UnmarshalJSON() func (rs *PushRuleset) MarshalJSON() ([]byte, error) { data := rawPushRuleset{ Override: rs.Override, Content: rs.Content, Room: rs.Room.unmap(), Sender: rs.Sender.unmap(), Underride: rs.Underride, } return json.Marshal(&data) } // 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) { // 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 { if match = pra.GetActions(room, event); match != nil { // Match found, return it. return } } // No match found, return default actions. return DefaultPushActions }