From 35b6c7bd276d2a6c7f09163d757a1c3cb885da79 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 21 Mar 2018 18:46:19 +0200 Subject: [PATCH] Add external debug file, refactoring and push rule parser --- config/session.go | 3 +- gomuks.go | 1 + interface/matrix.go | 2 +- matrix/ext/doc.go | 18 ++ matrix/ext/pushrules.go | 376 ++++++++++++++++++++++++++++++++++++++++ matrix/matrix.go | 26 ++- matrix/room/member.go | 2 +- matrix/room/room.go | 2 +- ui/debug/external.go | 45 +++++ 9 files changed, 469 insertions(+), 6 deletions(-) create mode 100644 matrix/ext/doc.go create mode 100644 matrix/ext/pushrules.go create mode 100644 ui/debug/external.go diff --git a/config/session.go b/config/session.go index de10757..2f2ff7c 100644 --- a/config/session.go +++ b/config/session.go @@ -22,6 +22,7 @@ import ( "path/filepath" "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/ext" rooms "maunium.net/go/gomuks/matrix/room" "maunium.net/go/gomuks/ui/debug" ) @@ -33,7 +34,7 @@ type Session struct { NextBatch string FilterID string Rooms map[string]*rooms.Room - PushRules *gomatrix.PushRuleset + PushRules *gomx_ext.PushRuleset } func (config *Config) LoadSession(mxid string) error { diff --git a/gomuks.go b/gomuks.go index 67b6989..b4c74fe 100644 --- a/gomuks.go +++ b/gomuks.go @@ -65,6 +65,7 @@ func NewGomuks(enableDebug bool) *Gomuks { main := gmx.ui.InitViews() if enableDebug { + debug.EnableExternal() main = gmx.debug.Wrap(main) gmx.debugMode = true } diff --git a/interface/matrix.go b/interface/matrix.go index c40639d..459f99d 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -33,5 +33,5 @@ type MatrixContainer interface { JoinRoom(roomID string) error LeaveRoom(roomID string) error GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) - GetRoom(roomID string) *room.Room + GetRoom(roomID string) *rooms.Room } diff --git a/matrix/ext/doc.go b/matrix/ext/doc.go new file mode 100644 index 0000000..21b818c --- /dev/null +++ b/matrix/ext/doc.go @@ -0,0 +1,18 @@ +// 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 gomx_ext contains extensions to the gomatrix package. +package gomx_ext diff --git a/matrix/ext/pushrules.go b/matrix/ext/pushrules.go new file mode 100644 index 0000000..6cb16d2 --- /dev/null +++ b/matrix/ext/pushrules.go @@ -0,0 +1,376 @@ +package gomx_ext + +import ( + "encoding/json" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/room" +) + +// GetPushRules returns the push notification rules for the given scope. +func GetPushRules(client *gomatrix.Client) (resp *PushRuleset, err error) { + u, _ := url.Parse(client.BuildURL("pushrules", "global")) + u.Path += "/" + _, err = client.MakeRequest("GET", u.String(), nil, &resp) + return +} + +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) +} + +func (rs *PushRuleset) GetActions(room *rooms.Room, event *gomatrix.Event) (match []*PushAction) { + 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 +} + +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) []*PushAction { + 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) []*PushAction { + 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 []*PushAction `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) +} + +type PushActionType string + +const ( + ActionNotify PushActionType = "notify" + ActionDontNotify PushActionType = "dont_notify" + ActionCoalesce PushActionType = "coalesce" + ActionSetTweak PushActionType = "set_tweak" +) + +type PushActionTweak string + +const ( + TweakSound PushActionTweak = "sound" + TweakHighlight PushActionTweak = "highlight" +) + +type PushAction struct { + Action PushActionType + Tweak PushActionTweak + Value string +} + +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"].(string) + } + } + return nil +} + +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) + } +} + +type PushKind string + +const ( + KindEventMatch PushKind = "event_match" + KindContainsDisplayName PushKind = "contains_display_name" + KindRoomMemberCount PushKind = "room_member_count" +) + +type PushCondition struct { + Kind PushKind `json:"kind"` + Key string `json:"key,omitempty"` + Pattern string `json:"pattern,omitempty"` + Is string `json:"string,omitempty"` +} + +var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") + +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 true + } +} + +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.Owner) + 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.Is, -1) + if len(groupGroups) != 1 { + return true + } + + operator := "==" + wantedMemberCount := 0 + + group := groupGroups[0] + if len(group) == 0 { + return true + } 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/matrix.go b/matrix/matrix.go index 556d567..7dbbbe1 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -24,7 +24,8 @@ import ( "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/interface" - rooms "maunium.net/go/gomuks/matrix/room" + "maunium.net/go/gomuks/matrix/ext" + "maunium.net/go/gomuks/matrix/room" "maunium.net/go/gomuks/notification" "maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/ui/types" @@ -120,13 +121,20 @@ func (c *Container) Client() *gomatrix.Client { } func (c *Container) UpdatePushRules() { - resp, err := c.client.PushRules() + resp, err := gomx_ext.GetPushRules(c.client) if err != nil { debug.Print("Failed to fetch push rules:", err) } c.config.Session.PushRules = resp } +func (c *Container) PushRules() *gomx_ext.PushRuleset { + if c.config.Session.PushRules == nil { + c.UpdatePushRules() + } + return c.config.Session.PushRules +} + func (c *Container) UpdateRoomList() { resp, err := c.client.JoinedRooms() if err != nil { @@ -200,6 +208,20 @@ func (c *Container) NotifyMessage(room *rooms.Room, message *types.Message) { func (c *Container) HandleMessage(evt *gomatrix.Event) { room, message := c.ui.MainView().ProcessMessageEvent(evt) if room != nil { + match := c.PushRules().GetActions(room.Room, evt) + + var buf strings.Builder + buf.WriteRune('[') + for i, rule := range match { + fmt.Fprintf(&buf, "{%s, %s, %s}", rule.Action, rule.Tweak, rule.Value) + if i < len(match)-1 { + buf.WriteRune(',') + buf.WriteRune(' ') + } + } + buf.WriteRune(']') + debug.Print(buf.String()) + c.NotifyMessage(room.Room, message) room.AddMessage(message, widget.AppendMessage) c.ui.Render() diff --git a/matrix/room/member.go b/matrix/room/member.go index 474d2fd..3b3a30c 100644 --- a/matrix/room/member.go +++ b/matrix/room/member.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package room +package rooms import ( "maunium.net/go/gomatrix" diff --git a/matrix/room/room.go b/matrix/room/room.go index 6bafbfa..92d6c5a 100644 --- a/matrix/room/room.go +++ b/matrix/room/room.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package room +package rooms import ( "fmt" diff --git a/ui/debug/external.go b/ui/debug/external.go new file mode 100644 index 0000000..8122b2a --- /dev/null +++ b/ui/debug/external.go @@ -0,0 +1,45 @@ +// 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 debug + +import ( + "fmt" + "io" + "os" +) + +var writer io.Writer + +func EnableExternal() { + var err error + writer, err = os.OpenFile("/tmp/gomuks-debug.log", os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + writer = nil + } +} + +func ExtPrintf(text string, args ...interface{}) { + if writer != nil { + fmt.Fprintf(writer, text + "\n", args...) + } +} + +func ExtPrint(text ...interface{}) { + if writer != nil { + fmt.Fprintln(writer, text...) + } +}