From 9fd67102ad2cca16c092e23ffd928b77ab08d7e0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 21 Mar 2018 23:29:58 +0200 Subject: Refactoring and godocs --- matrix/pushrules/action.go | 135 +++++++++++++++++++++++++++++++++++++ matrix/pushrules/condition.go | 147 +++++++++++++++++++++++++++++++++++++++++ matrix/pushrules/doc.go | 2 + matrix/pushrules/pushrules.go | 42 ++++++++++++ matrix/pushrules/rule.go | 150 ++++++++++++++++++++++++++++++++++++++++++ matrix/pushrules/ruleset.go | 87 ++++++++++++++++++++++++ 6 files changed, 563 insertions(+) create mode 100644 matrix/pushrules/action.go create mode 100644 matrix/pushrules/condition.go create mode 100644 matrix/pushrules/doc.go create mode 100644 matrix/pushrules/pushrules.go create mode 100644 matrix/pushrules/rule.go create mode 100644 matrix/pushrules/ruleset.go (limited to 'matrix/pushrules') diff --git a/matrix/pushrules/action.go b/matrix/pushrules/action.go new file mode 100644 index 0000000..1de973f --- /dev/null +++ b/matrix/pushrules/action.go @@ -0,0 +1,135 @@ +// 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 . + +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) + } +} diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go new file mode 100644 index 0000000..ecbf5b2 --- /dev/null +++ b/matrix/pushrules/condition.go @@ -0,0 +1,147 @@ +// 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 . + +package pushrules + +import ( + "regexp" + "strconv" + "strings" + + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/room" +) + +// 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 *rooms.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 *rooms.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, err := glob.Compile(cond.Pattern) + if err != nil { + return false + } + + 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 *rooms.Room, event *gomatrix.Event) bool { + member := room.GetMember(room.SessionUserID) + if member == nil { + 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 { + 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]) + } + + memberCount := len(room.GetMembers()) + + switch operator { + case "==": + return wantedMemberCount == memberCount + case ">": + return wantedMemberCount > memberCount + case ">=": + return wantedMemberCount >= memberCount + case "<": + return wantedMemberCount < memberCount + case "<=": + return wantedMemberCount <= memberCount + default: + return false + } +} diff --git a/matrix/pushrules/doc.go b/matrix/pushrules/doc.go new file mode 100644 index 0000000..19cd774 --- /dev/null +++ b/matrix/pushrules/doc.go @@ -0,0 +1,2 @@ +// Package pushrules contains utilities to parse push notification rules. +package pushrules diff --git a/matrix/pushrules/pushrules.go b/matrix/pushrules/pushrules.go new file mode 100644 index 0000000..bfcb4ef --- /dev/null +++ b/matrix/pushrules/pushrules.go @@ -0,0 +1,42 @@ +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 +} + diff --git a/matrix/pushrules/rule.go b/matrix/pushrules/rule.go new file mode 100644 index 0000000..067bc95 --- /dev/null +++ b/matrix/pushrules/rule.go @@ -0,0 +1,150 @@ +// 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 . + +package pushrules + +import ( + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/room" +) + +type PushRuleArray []*PushRule + +func (rules PushRuleArray) setType(typ PushRuleType) PushRuleArray { + for _, rule := range rules { + rule.Type = typ + } + return rules +} + +func (rules PushRuleArray) GetActions(room *rooms.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 *rooms.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 *rooms.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 *rooms.Room, event *gomatrix.Event) bool { + for _, cond := range rule.Conditions { + if !cond.Match(room, event) { + return false + } + } + return true +} + +func (rule *PushRule) matchPattern(room *rooms.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) +} diff --git a/matrix/pushrules/ruleset.go b/matrix/pushrules/ruleset.go new file mode 100644 index 0000000..6708b70 --- /dev/null +++ b/matrix/pushrules/ruleset.go @@ -0,0 +1,87 @@ +// 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 . + +package pushrules + +import ( + "encoding/json" + + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/room" +) + +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"` +} + +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 +} + +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) +} + +var DefaultPushActions = make(PushActionArray, 0) + +func (rs *PushRuleset) GetActions(room *rooms.Room, event *gomatrix.Event) (match PushActionArray) { + if match = rs.Override.GetActions(room, event); match != nil { + return + } + if match = rs.Content.GetActions(room, event); match != nil { + return + } + if match = rs.Room.GetActions(room, event); match != nil { + return + } + if match = rs.Sender.GetActions(room, event); match != nil { + return + } + if match = rs.Underride.GetActions(room, event); match != nil { + return + } + return DefaultPushActions +} -- cgit v1.2.3