Add external debug file, refactoring and push rule parser

This commit is contained in:
Tulir Asokan 2018-03-21 18:46:19 +02:00
parent 7994c289aa
commit 35b6c7bd27
9 changed files with 469 additions and 6 deletions

View File

@ -22,6 +22,7 @@ import (
"path/filepath" "path/filepath"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/matrix/ext"
rooms "maunium.net/go/gomuks/matrix/room" rooms "maunium.net/go/gomuks/matrix/room"
"maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/ui/debug"
) )
@ -33,7 +34,7 @@ type Session struct {
NextBatch string NextBatch string
FilterID string FilterID string
Rooms map[string]*rooms.Room Rooms map[string]*rooms.Room
PushRules *gomatrix.PushRuleset PushRules *gomx_ext.PushRuleset
} }
func (config *Config) LoadSession(mxid string) error { func (config *Config) LoadSession(mxid string) error {

View File

@ -65,6 +65,7 @@ func NewGomuks(enableDebug bool) *Gomuks {
main := gmx.ui.InitViews() main := gmx.ui.InitViews()
if enableDebug { if enableDebug {
debug.EnableExternal()
main = gmx.debug.Wrap(main) main = gmx.debug.Wrap(main)
gmx.debugMode = true gmx.debugMode = true
} }

View File

@ -33,5 +33,5 @@ type MatrixContainer interface {
JoinRoom(roomID string) error JoinRoom(roomID string) error
LeaveRoom(roomID string) error LeaveRoom(roomID string) error
GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error)
GetRoom(roomID string) *room.Room GetRoom(roomID string) *rooms.Room
} }

18
matrix/ext/doc.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
// Package gomx_ext contains extensions to the gomatrix package.
package gomx_ext

376
matrix/ext/pushrules.go Normal file
View File

@ -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
}
}

View File

@ -24,7 +24,8 @@ import (
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface" "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/notification"
"maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/ui/debug"
"maunium.net/go/gomuks/ui/types" "maunium.net/go/gomuks/ui/types"
@ -120,13 +121,20 @@ func (c *Container) Client() *gomatrix.Client {
} }
func (c *Container) UpdatePushRules() { func (c *Container) UpdatePushRules() {
resp, err := c.client.PushRules() resp, err := gomx_ext.GetPushRules(c.client)
if err != nil { if err != nil {
debug.Print("Failed to fetch push rules:", err) debug.Print("Failed to fetch push rules:", err)
} }
c.config.Session.PushRules = resp 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() { func (c *Container) UpdateRoomList() {
resp, err := c.client.JoinedRooms() resp, err := c.client.JoinedRooms()
if err != nil { if err != nil {
@ -200,6 +208,20 @@ func (c *Container) NotifyMessage(room *rooms.Room, message *types.Message) {
func (c *Container) HandleMessage(evt *gomatrix.Event) { func (c *Container) HandleMessage(evt *gomatrix.Event) {
room, message := c.ui.MainView().ProcessMessageEvent(evt) room, message := c.ui.MainView().ProcessMessageEvent(evt)
if room != nil { 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) c.NotifyMessage(room.Room, message)
room.AddMessage(message, widget.AppendMessage) room.AddMessage(message, widget.AppendMessage)
c.ui.Render() c.ui.Render()

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package room package rooms
import ( import (
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package room package rooms
import ( import (
"fmt" "fmt"

45
ui/debug/external.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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...)
}
}