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 --- config/config.go | 4 +- config/session.go | 16 +- gomuks.go | 6 +- matrix/ext/doc.go | 18 -- matrix/ext/pushrules.go | 438 ------------------------------------------ matrix/matrix.go | 51 +++-- 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 +++++++++ matrix/room/member.go | 28 ++- matrix/room/room.go | 17 +- matrix/sync.go | 32 +-- ui/view-login.go | 2 +- ui/view-main.go | 2 +- 17 files changed, 673 insertions(+), 504 deletions(-) delete mode 100644 matrix/ext/doc.go delete mode 100644 matrix/ext/pushrules.go 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 diff --git a/config/config.go b/config/config.go index 858f1d2..7d99274 100644 --- a/config/config.go +++ b/config/config.go @@ -27,8 +27,8 @@ import ( ) type Config struct { - MXID string `yaml:"mxid"` - HS string `yaml:"homeserver"` + UserID string `yaml:"mxid"` + HS string `yaml:"homeserver"` dir string `yaml:"-"` Session *Session `yaml:"-"` diff --git a/config/session.go b/config/session.go index 631c954..3fdc169 100644 --- a/config/session.go +++ b/config/session.go @@ -22,19 +22,19 @@ import ( "path/filepath" "maunium.net/go/gomatrix" - "maunium.net/go/gomuks/matrix/ext" + "maunium.net/go/gomuks/matrix/pushrules" rooms "maunium.net/go/gomuks/matrix/room" "maunium.net/go/gomuks/ui/debug" ) type Session struct { - MXID string `json:"-"` + UserID string `json:"-"` path string `json:"-"` AccessToken string NextBatch string FilterID string Rooms map[string]*rooms.Room - PushRules *gomx_ext.PushRuleset + PushRules *pushrules.PushRuleset } func (config *Config) LoadSession(mxid string) error { @@ -44,9 +44,9 @@ func (config *Config) LoadSession(mxid string) error { func (config *Config) NewSession(mxid string) *Session { return &Session{ - MXID: mxid, - path: filepath.Join(config.dir, mxid+".session"), - Rooms: make(map[string]*rooms.Room), + UserID: mxid, + path: filepath.Join(config.dir, mxid+".session"), + Rooms: make(map[string]*rooms.Room), } } @@ -76,7 +76,7 @@ func (s *Session) Load() error { func (s *Session) Save() error { data, err := json.Marshal(s) if err != nil { - debug.Print("Failed to marshal session of", s.MXID, err) + debug.Print("Failed to marshal session of", s.UserID, err) return err } @@ -99,7 +99,7 @@ func (s *Session) LoadNextBatch(_ string) string { func (s *Session) GetRoom(mxid string) *rooms.Room { room, _ := s.Rooms[mxid] if room == nil { - room = rooms.NewRoom(mxid, s.MXID) + room = rooms.NewRoom(mxid, s.UserID) s.Rooms[room.ID] = room } return room diff --git a/gomuks.go b/gomuks.go index b4c74fe..6429f24 100644 --- a/gomuks.go +++ b/gomuks.go @@ -54,11 +54,11 @@ func NewGomuks(enableDebug bool) *Gomuks { gmx.config = config.NewConfig(configDir) gmx.ui = ui.NewGomuksUI(gmx) - gmx.matrix = matrix.NewMatrixContainer(gmx) + gmx.matrix = matrix.NewContainer(gmx) gmx.config.Load() - if len(gmx.config.MXID) > 0 { - gmx.config.LoadSession(gmx.config.MXID) + if len(gmx.config.UserID) > 0 { + gmx.config.LoadSession(gmx.config.UserID) } gmx.matrix.InitClient() diff --git a/matrix/ext/doc.go b/matrix/ext/doc.go deleted file mode 100644 index 21b818c..0000000 --- a/matrix/ext/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index ccccbb0..0000000 --- a/matrix/ext/pushrules.go +++ /dev/null @@ -1,438 +0,0 @@ -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 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 -} - -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 -} - -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 -} - -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) -} - -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 PushActionArray []*PushAction - -type PushActionArrayShould struct { - NotifySpecified bool - Notify bool - Highlight bool - - PlaySound bool - SoundName string -} - -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 -} - -type PushAction struct { - Action PushActionType - Tweak PushActionTweak - Value interface{} -} - -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 -} - -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 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.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 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/matrix.go b/matrix/matrix.go index 6ab922c..a73e050 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -25,13 +25,16 @@ import ( "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/matrix/ext" + "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/room" "maunium.net/go/gomuks/notification" "maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/ui/widget" ) +// Container is a wrapper for a gomatrix Client and some other stuff. +// +// It is used for all Matrix calls from the UI and Matrix event handlers. type Container struct { client *gomatrix.Client gmx ifc.Gomuks @@ -43,7 +46,8 @@ type Container struct { typing int64 } -func NewMatrixContainer(gmx ifc.Gomuks) *Container { +// NewContainer creates a new Container for the given Gomuks instance. +func NewContainer(gmx ifc.Gomuks) *Container { c := &Container{ config: gmx.Config(), ui: gmx.UI(), @@ -53,6 +57,7 @@ func NewMatrixContainer(gmx ifc.Gomuks) *Container { return c } +// InitClient initializes the gomatrix client and connects to the homeserver specified in the config. func (c *Container) InitClient() error { if len(c.config.HS) == 0 { return fmt.Errorf("no homeserver in config") @@ -66,7 +71,7 @@ func (c *Container) InitClient() error { var mxid, accessToken string if c.config.Session != nil { accessToken = c.config.Session.AccessToken - mxid = c.config.MXID + mxid = c.config.UserID } var err error @@ -77,16 +82,18 @@ func (c *Container) InitClient() error { c.stop = make(chan bool, 1) - if c.config.Session != nil && len(c.config.Session.AccessToken) > 0 { + if c.config.Session != nil && len(accessToken) > 0 { go c.Start() } return nil } +// Initialized returns whether or not the gomatrix client is initialized (see InitClient()) func (c *Container) Initialized() bool { return c.client != nil } +// Login sends a password login request with the given username and password. func (c *Container) Login(user, password string) error { resp, err := c.client.Login(&gomatrix.ReqLogin{ Type: "m.login.password", @@ -97,7 +104,7 @@ func (c *Container) Login(user, password string) error { return err } c.client.SetCredentials(resp.UserID, resp.AccessToken) - c.config.MXID = resp.UserID + c.config.UserID = resp.UserID c.config.Save() c.config.Session = c.config.NewSession(resp.UserID) @@ -109,6 +116,7 @@ func (c *Container) Login(user, password string) error { return nil } +// Stop stops the Matrix syncer. func (c *Container) Stop() { if c.running { c.stop <- true @@ -116,26 +124,30 @@ func (c *Container) Stop() { } } +// Client returns the underlying gomatrix client object. func (c *Container) Client() *gomatrix.Client { return c.client } +// UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object. func (c *Container) UpdatePushRules() { debug.Print("Updating push rules...") - resp, err := gomx_ext.GetPushRules(c.client) + resp, err := pushrules.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 { +// PushRules returns the push notification rules. If no push rules are cached, UpdatePushRules() will be called first. +func (c *Container) PushRules() *pushrules.PushRuleset { if c.config.Session.PushRules == nil { c.UpdatePushRules() } return c.config.Session.PushRules } +// UpdateRoomList fetches the list of rooms the user has joined and sends them to the UI. func (c *Container) UpdateRoomList() { resp, err := c.client.JoinedRooms() if err != nil { @@ -151,11 +163,13 @@ func (c *Container) UpdateRoomList() { c.ui.MainView().SetRooms(resp.JoinedRooms) } +// OnLogout stops the syncer and moves the UI back to the login view. func (c *Container) OnLogout() { c.Stop() c.ui.SetView(ifc.ViewLogin) } +// OnLogin initializes the syncer and updates the room list. func (c *Container) OnLogin() { c.client.Store = c.config.Session @@ -169,6 +183,7 @@ func (c *Container) OnLogin() { c.UpdateRoomList() } +// Start moves the UI to the main view, calls OnLogin() and runs the syncer forever until stopped with Stop() func (c *Container) Start() { defer c.gmx.Recover() @@ -197,6 +212,7 @@ func (c *Container) Start() { } } +// NotifyMessage sends a desktop notification of the message with the given details. func (c *Container) NotifyMessage(room *rooms.Room, sender, text string, critical bool) { if room.GetTitle() != sender { sender = fmt.Sprintf("%s (%s)", sender, room.GetTitle()) @@ -204,11 +220,12 @@ func (c *Container) NotifyMessage(room *rooms.Room, sender, text string, critica notification.Send(sender, text, critical) } +// HandleMessage is the event handler for the m.room.message timeline event. func (c *Container) HandleMessage(evt *gomatrix.Event) { room, message := c.ui.MainView().ProcessMessageEvent(evt) if room != nil { pushRules := c.PushRules().GetActions(room.Room, evt).Should() - if (pushRules.Notify || !pushRules.NotifySpecified) && evt.Sender != c.config.Session.MXID { + if (pushRules.Notify || !pushRules.NotifySpecified) && evt.Sender != c.config.Session.UserID { c.NotifyMessage(room.Room, message.Sender, message.Text, pushRules.Highlight) } if pushRules.Highlight { @@ -222,15 +239,17 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) { } } +// HandlePushRules is the event handler for the m.push_rules account data event. func (c *Container) HandlePushRules(evt *gomatrix.Event) { debug.Print("Received updated push rules") var err error - c.config.Session.PushRules, err = gomx_ext.EventToPushRules(evt) + c.config.Session.PushRules, err = pushrules.EventToPushRules(evt) if err != nil { debug.Print("Failed to convert event to push rules:", err) } } +// HandleMembership is the event handler for the m.room.membership state event. func (c *Container) HandleMembership(evt *gomatrix.Event) { const Hour = 1 * 60 * 60 * 1000 if evt.Unsigned.Age > Hour { @@ -249,6 +268,7 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) { } } +// HandleTyping is the event handler for the m.typing event. func (c *Container) HandleTyping(evt *gomatrix.Event) { users := evt.Content["user_ids"].([]interface{}) @@ -259,6 +279,7 @@ func (c *Container) HandleTyping(evt *gomatrix.Event) { c.ui.MainView().SetTyping(evt.RoomID, strUsers) } +// SendMessage sends a message with the given text to the given room. func (c *Container) SendMessage(roomID, text string) (string, error) { defer c.gmx.Recover() c.SendTyping(roomID, false) @@ -269,6 +290,7 @@ func (c *Container) SendMessage(roomID, text string) (string, error) { return resp.EventID, nil } +// SendTyping sets whether or not the user is typing in the given room. func (c *Container) SendTyping(roomID string, typing bool) { defer c.gmx.Recover() ts := time.Now().Unix() @@ -277,14 +299,15 @@ func (c *Container) SendTyping(roomID string, typing bool) { } if typing { - c.client.UserTyping(roomID, true, 5000) - c.typing = ts + 5 + c.client.UserTyping(roomID, true, 20000) + c.typing = ts + 15 } else { c.client.UserTyping(roomID, false, 0) c.typing = 0 } } +// JoinRoom makes the current user try to join the given room. func (c *Container) JoinRoom(roomID string) error { if len(roomID) == 0 { return fmt.Errorf("invalid room ID") @@ -303,6 +326,7 @@ func (c *Container) JoinRoom(roomID string) error { return nil } +// LeaveRoom makes the current user leave the given room. func (c *Container) LeaveRoom(roomID string) error { if len(roomID) == 0 { return fmt.Errorf("invalid room ID") @@ -316,6 +340,7 @@ func (c *Container) LeaveRoom(roomID string) error { return nil } +// getState requests the state of the given room. func (c *Container) getState(roomID string) []*gomatrix.Event { content := make([]*gomatrix.Event, 0) err := c.client.StateEvent(roomID, "", "", &content) @@ -326,6 +351,7 @@ func (c *Container) getState(roomID string) []*gomatrix.Event { return content } +// GetHistory fetches room history. func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) { resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit) if err != nil { @@ -334,6 +360,9 @@ func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix. return resp.Chunk, resp.End, nil } +// GetRoom gets the room instance stored in the session. +// +// If the room doesn't have any state events stored, its state will be updated. func (c *Container) GetRoom(roomID string) *rooms.Room { room := c.config.Session.GetRoom(roomID) if room != nil && len(room.State) == 0 { 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 +} diff --git a/matrix/room/member.go b/matrix/room/member.go index 3b3a30c..4af20a6 100644 --- a/matrix/room/member.go +++ b/matrix/room/member.go @@ -20,18 +20,34 @@ import ( "maunium.net/go/gomatrix" ) +type Membership string + +// The allowed membership states as specified in spec section 10.5.5. +const ( + MembershipJoin Membership = "join" + MembershipLeave Membership = "leave" + MembershipInvite Membership = "invite" + MembershipKnock Membership = "knock" +) + +// Member represents a member in a room. type Member struct { - UserID string `json:"-"` - Membership string `json:"membership"` - DisplayName string `json:"displayname"` - AvatarURL string `json:"avatar_url"` + // The MXID of the member. + UserID string `json:"-"` + // The membership status. Defaults to leave. + Membership Membership `json:"membership"` + // The display name of the user. Defaults to the user ID. + DisplayName string `json:"displayname"` + // The avatar URL of the user. Defaults to an empty string. + AvatarURL string `json:"avatar_url"` } +// eventToRoomMember converts a m.room.member state event into a Member object. func eventToRoomMember(userID string, event *gomatrix.Event) *Member { if event == nil { return &Member{ UserID: userID, - Membership: "leave", + Membership: MembershipLeave, } } membership, _ := event.Content["membership"].(string) @@ -44,7 +60,7 @@ func eventToRoomMember(userID string, event *gomatrix.Event) *Member { return &Member{ UserID: userID, - Membership: membership, + Membership: Membership(membership), DisplayName: displayName, AvatarURL: avatarURL, } diff --git a/matrix/room/room.go b/matrix/room/room.go index 92d6c5a..264ee3b 100644 --- a/matrix/room/room.go +++ b/matrix/room/room.go @@ -26,11 +26,18 @@ import ( type Room struct { *gomatrix.Room + // The first batch of events that has been fetched for this room. + // Used for fetching additional history. PrevBatch string - Owner string + // The MXID of the user whose session this room was created for. + SessionUserID string + // MXID -> Member cache calculated from membership events. memberCache map[string]*Member + // The first non-SessionUserID member in the room. Calculated at the same time as memberCache. firstMemberCache string + // The name of the room. Calculated from the state event name, canonical_alias or alias or the member cache. nameCache string + // The topic of the room. Directly fetched from the m.room.topic state event. topicCache string } @@ -135,7 +142,7 @@ func (room *Room) updateNameFromMembers() { } // updateNameCache updates the room display name based on the room state in the order -// specified in section 11.2.2.5 of r0.3.0 of the Client-Server API specification. +// specified in spec section 11.2.2.5. func (room *Room) updateNameCache() { if len(room.nameCache) == 0 { room.updateNameFromNameEvent() @@ -167,7 +174,7 @@ func (room *Room) createMemberCache() map[string]*Member { room.firstMemberCache = "" if events != nil { for userID, event := range events { - if len(room.firstMemberCache) == 0 && userID != room.Owner { + if len(room.firstMemberCache) == 0 && userID != room.SessionUserID { room.firstMemberCache = userID } member := eventToRoomMember(userID, event) @@ -204,7 +211,7 @@ func (room *Room) GetMember(userID string) *Member { // NewRoom creates a new Room with the given ID func NewRoom(roomID, owner string) *Room { return &Room{ - Room: gomatrix.NewRoom(roomID), - Owner: owner, + Room: gomatrix.NewRoom(roomID), + SessionUserID: owner, } } diff --git a/matrix/sync.go b/matrix/sync.go index 9eeb87a..a5f3b91 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -1,3 +1,21 @@ +// 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 . + +// Based on https://github.com/matrix-org/gomatrix/blob/master/sync.go + package matrix import ( @@ -26,15 +44,16 @@ func NewGomuksSyncer(session *config.Session) *GomuksSyncer { } } +// ProcessResponse processes a Matrix sync response. func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) { - if !s.shouldProcessResponse(res, since) { + if len(since) == 0 { return } // gdebug.Print("Processing sync response", since, res) defer func() { if r := recover(); r != nil { - err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.Session.MXID, since, r, debug.Stack()) + err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.Session.UserID, since, r, debug.Stack()) } }() @@ -99,15 +118,6 @@ func (s *GomuksSyncer) OnEventType(eventType string, callback gomatrix.OnEventLi s.listeners[eventType] = append(s.listeners[eventType], callback) } -// shouldProcessResponse returns true if the response should be processed. May modify the response to remove -// stuff that shouldn't be processed. -func (s *GomuksSyncer) shouldProcessResponse(resp *gomatrix.RespSync, since string) bool { - if since == "" { - return false - } - return true -} - func (s *GomuksSyncer) notifyListeners(event *gomatrix.Event) { listeners, exists := s.listeners[event.Type] if !exists { diff --git a/ui/view-login.go b/ui/view-login.go index c46ad25..4ae673c 100644 --- a/ui/view-login.go +++ b/ui/view-login.go @@ -29,7 +29,7 @@ func (ui *GomuksUI) NewLoginView() tview.Primitive { } homeserver := widget.NewAdvancedInputField().SetLabel("Homeserver").SetText(hs).SetFieldWidth(30) - username := widget.NewAdvancedInputField().SetLabel("Username").SetText(ui.gmx.Config().MXID).SetFieldWidth(30) + username := widget.NewAdvancedInputField().SetLabel("Username").SetText(ui.gmx.Config().UserID).SetFieldWidth(30) password := widget.NewAdvancedInputField().SetLabel("Password").SetMaskCharacter('*').SetFieldWidth(30) ui.loginView = tview.NewForm() diff --git a/ui/view-main.go b/ui/view-main.go index e694d9c..8ff925c 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -418,7 +418,7 @@ func (view *MainView) processOwnMembershipChange(evt *gomatrix.Event) { } func (view *MainView) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (room *widget.RoomView, message *types.Message) { - if new && evt.StateKey != nil && *evt.StateKey == view.config.Session.MXID { + if new && evt.StateKey != nil && *evt.StateKey == view.config.Session.UserID { view.processOwnMembershipChange(evt) } -- cgit v1.2.3