diff options
47 files changed, 624 insertions, 3438 deletions
diff --git a/config/config.go b/config/config.go index e62a8bd..634120b 100644 --- a/config/config.go +++ b/config/config.go @@ -26,9 +26,10 @@ import ( "gopkg.in/yaml.v2" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules" "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" ) @@ -52,9 +53,9 @@ type UserPreferences struct { // Config contains the main config of gomuks. type Config struct { - UserID string `yaml:"mxid"` - AccessToken string `yaml:"access_token"` - HS string `yaml:"homeserver"` + UserID id.UserID `yaml:"mxid"` + AccessToken string `yaml:"access_token"` + HS string `yaml:"homeserver"` RoomCacheSize int `yaml:"room_cache_size"` RoomCacheAge int64 `yaml:"room_cache_age"` @@ -242,36 +243,36 @@ func (config *Config) save(name, dir, file string, source interface{}) { } } -func (config *Config) GetUserID() string { +func (config *Config) GetUserID() id.UserID { return config.UserID } -func (config *Config) SaveFilterID(_, filterID string) { +func (config *Config) SaveFilterID(_ id.UserID, filterID string) { config.AuthCache.FilterID = filterID config.SaveAuthCache() } -func (config *Config) LoadFilterID(_ string) string { +func (config *Config) LoadFilterID(_ id.UserID) string { return config.AuthCache.FilterID } -func (config *Config) SaveNextBatch(_, nextBatch string) { +func (config *Config) SaveNextBatch(_ id.UserID, nextBatch string) { config.AuthCache.NextBatch = nextBatch config.SaveAuthCache() } -func (config *Config) LoadNextBatch(_ string) string { +func (config *Config) LoadNextBatch(_ id.UserID) string { return config.AuthCache.NextBatch } -func (config *Config) SaveRoom(room *mautrix.Room) { +func (config *Config) SaveRoom(_ *mautrix.Room) { panic("SaveRoom is not supported") } -func (config *Config) LoadRoom(roomID string) *mautrix.Room { +func (config *Config) LoadRoom(_ id.RoomID) *mautrix.Room { panic("LoadRoom is not supported") } -func (config *Config) GetRoom(roomID string) *rooms.Room { +func (config *Config) GetRoom(roomID id.RoomID) *rooms.Room { return config.Rooms.GetOrCreate(roomID) } diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 4aa076f..0000000 --- a/config/config_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package config_test - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/config" -) - -func TestNewConfig_Defaults(t *testing.T) { - cfg := config.NewConfig("/tmp/gomuks-test-0", "/tmp/gomuks-test-0") - assert.Equal(t, "/tmp/gomuks-test-0", cfg.Dir) - assert.Equal(t, "/tmp/gomuks-test-0/history.db", cfg.HistoryPath) - assert.Equal(t, "/tmp/gomuks-test-0/media", cfg.MediaDir) -} - -func TestConfig_Load_NonexistentDoesntFail(t *testing.T) { - cfg := config.NewConfig("/tmp/gomuks-test-1", "/tmp/gomuks-test-1") - - defer os.RemoveAll("/tmp/gomuks-test-1") - - cfg.Load() - - stat, err := os.Stat(cfg.MediaDir) - assert.Nil(t, err) - assert.True(t, stat.IsDir()) - - /* FIXME - stat, err = os.Stat(cfg.HistoryDir) - assert.Nil(t, err) - assert.True(t, stat.IsDir())*/ -} - -func TestConfig_Load_DirectoryFails(t *testing.T) { - os.MkdirAll("/tmp/gomuks-test-2/config.yaml", 0700) - cfg := config.NewConfig("/tmp/gomuks-test-2", "/tmp/gomuks-test-2") - - defer os.RemoveAll("/tmp/gomuks-test-2") - defer func() { - if err := recover(); err == nil { - t.Fatalf("Load() didn't panic") - } - }() - - cfg.Load() -} - -func TestConfig_Load_ExistingFileIsLoaded(t *testing.T) { - os.MkdirAll("/tmp/gomuks-test-3", 0700) - ioutil.WriteFile("/tmp/gomuks-test-3/config.yaml", []byte(`{ - "mxid": "foo", - "homeserver": "bar", - "history_path": "/tmp/gomuks-test-3/foo.db", - "media_dir": "/tmp/gomuks-test-3/bar" - }`), 0700) - cfg := config.NewConfig("/tmp/gomuks-test-3", "/tmp/gomuks-test-3") - - defer os.RemoveAll("/tmp/gomuks-test-3") - - cfg.Load() - - assert.Equal(t, "foo", cfg.UserID) - assert.Equal(t, "bar", cfg.HS) - assert.Equal(t, "/tmp/gomuks-test-3/foo.db", cfg.HistoryPath) - assert.Equal(t, "/tmp/gomuks-test-3/bar", cfg.MediaDir) - - stat, err := os.Stat(cfg.MediaDir) - assert.Nil(t, err) - assert.True(t, stat.IsDir()) - - /* FIXME - stat, err = os.Stat(cfg.HistoryDir) - assert.Nil(t, err) - assert.True(t, stat.IsDir())*/ -} - -func TestConfig_Load_InvalidExistingFilePanics(t *testing.T) { - os.MkdirAll("/tmp/gomuks-test-4", 0700) - ioutil.WriteFile("/tmp/gomuks-test-4/config.yaml", []byte(`this is not JSON.`), 0700) - cfg := config.NewConfig("/tmp/gomuks-test-4", "/tmp/gomuks-test-4") - - defer os.RemoveAll("/tmp/gomuks-test-4") - defer func() { - if err := recover(); err == nil { - t.Fatalf("Load() didn't panic") - } - }() - - cfg.Load() -} - -func TestConfig_Clear(t *testing.T) { - cfg := config.NewConfig("/tmp/gomuks-test-5", "/tmp/gomuks-test-5") - - defer os.RemoveAll("/tmp/gomuks-test-5") - - cfg.Load() - - stat, err := os.Stat(cfg.MediaDir) - assert.Nil(t, err) - assert.True(t, stat.IsDir()) - - /* FIXME - stat, err = os.Stat(cfg.HistoryDir) - assert.Nil(t, err) - assert.True(t, stat.IsDir())*/ - - cfg.Clear() - - stat, err = os.Stat(cfg.MediaDir) - assert.True(t, os.IsNotExist(err)) - assert.Nil(t, stat) - - /* FIXME - stat, err = os.Stat(cfg.HistoryDir) - assert.True(t, os.IsNotExist(err)) - assert.Nil(t, stat)*/ -} - -func TestConfig_Save(t *testing.T) { - cfg := config.NewConfig("/tmp/gomuks-test-6", "/tmp/gomuks-test-6") - - defer os.RemoveAll("/tmp/gomuks-test-6") - - cfg.Load() - cfg.Save() - - dat, err := ioutil.ReadFile("/tmp/gomuks-test-6/config.yaml") - assert.Nil(t, err) - assert.Contains(t, string(dat), "/tmp/gomuks-test-6") -} diff --git a/interface/matrix.go b/interface/matrix.go index 34d8f7f..4be21fd 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -17,16 +17,18 @@ package ifc import ( - "maunium.net/go/gomuks/config" - "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/gomuks/config" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/gomuks/matrix/rooms" ) type Relation struct { - Type mautrix.RelationType - Event *event.Event + Type event.RelationType + Event *muksevt.Event } type MatrixContainer interface { @@ -42,23 +44,23 @@ type MatrixContainer interface { Logout() SendPreferencesToMatrix() - PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, relation *Relation) *event.Event - SendEvent(evt *event.Event) (string, error) - Redact(roomID, eventID, reason string) error - SendTyping(roomID string, typing bool) - MarkRead(roomID, eventID string) - JoinRoom(roomID, server string) (*rooms.Room, error) - LeaveRoom(roomID string) error + PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, relation *Relation) *muksevt.Event + SendEvent(evt *muksevt.Event) (id.EventID, error) + Redact(roomID id.RoomID, eventID id.EventID, reason string) error + SendTyping(roomID id.RoomID, typing bool) + MarkRead(roomID id.RoomID, eventID id.EventID) + JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) + LeaveRoom(roomID id.RoomID) error CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) FetchMembers(room *rooms.Room) error - GetHistory(room *rooms.Room, limit int) ([]*event.Event, error) - GetEvent(room *rooms.Room, eventID string) (*event.Event, error) - GetRoom(roomID string) *rooms.Room - GetOrCreateRoom(roomID string) *rooms.Room + GetHistory(room *rooms.Room, limit int) ([]*muksevt.Event, error) + GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error) + GetRoom(roomID id.RoomID) *rooms.Room + GetOrCreateRoom(roomID id.RoomID) *rooms.Room - Download(uri mautrix.ContentURI) ([]byte, error) - DownloadToDisk(uri mautrix.ContentURI, target string) (string, error) - GetDownloadURL(uri mautrix.ContentURI) string - GetCachePath(uri mautrix.ContentURI) string + Download(uri id.ContentURI) ([]byte, error) + DownloadToDisk(uri id.ContentURI, target string) (string, error) + GetDownloadURL(uri id.ContentURI) string + GetCachePath(uri id.ContentURI) string } diff --git a/interface/ui.go b/interface/ui.go index abc108f..67cc35c 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -19,9 +19,10 @@ package ifc import ( "time" - "maunium.net/go/gomuks/matrix/event" - "maunium.net/go/gomuks/matrix/pushrules" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules" ) type UIProvider func(gmx Gomuks) GomuksUI @@ -40,7 +41,7 @@ type GomuksUI interface { } type MainView interface { - GetRoom(roomID string) RoomView + GetRoom(roomID id.RoomID) RoomView AddRoom(room *rooms.Room) RemoveRoom(room *rooms.Room) SetRooms(rooms *rooms.RoomCache) @@ -48,7 +49,7 @@ type MainView interface { UpdateTags(room *rooms.Room) - SetTyping(roomID string, users []string) + SetTyping(roomID id.RoomID, users []id.UserID) NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) } @@ -57,23 +58,23 @@ type RoomView interface { MxRoom() *rooms.Room SetCompletions(completions []string) - SetTyping(users []string) + SetTyping(users []id.UserID) UpdateUserList() - AddEvent(evt *event.Event) Message - AddRedaction(evt *event.Event) - AddEdit(evt *event.Event) - AddReaction(evt *event.Event, key string) - GetEvent(eventID string) Message + AddEvent(evt *muksevt.Event) Message + AddRedaction(evt *muksevt.Event) + AddEdit(evt *muksevt.Event) + AddReaction(evt *muksevt.Event, key string) + GetEvent(eventID id.EventID) Message AddServiceMessage(message string) } type Message interface { - ID() string + ID() id.EventID Time() time.Time NotificationSenderName() string NotificationContent() string SetIsHighlight(highlight bool) - SetID(id string) + SetID(id id.EventID) } diff --git a/lib/glob/LICENSE b/lib/glob/LICENSE deleted file mode 100644 index cb00d95..0000000 --- a/lib/glob/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Glob is licensed under the MIT "Expat" License: - -Copyright (c) 2016: Zachary Yedidia. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/glob/README.md b/lib/glob/README.md deleted file mode 100644 index e2e6c64..0000000 --- a/lib/glob/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# String globbing in Go - -[![GoDoc](https://godoc.org/github.com/zyedidia/glob?status.svg)](http://godoc.org/github.com/zyedidia/glob) - -This package adds support for globs in Go. - -It simply converts glob expressions to regexps. I try to follow the standard defined [here](http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13). - -# Example - -```go -package main - -import "github.com/zyedidia/glob" - -func main() { - glob, err := glob.Compile("{*.go,*.c}") - if err != nil { - // Error - } - - glob.Match([]byte("test.c")) // true - glob.Match([]byte("hello.go")) // true - glob.Match([]byte("test.d")) // false -} -``` - -You can call all the same functions on a glob that you can call on a regexp. diff --git a/lib/glob/glob.go b/lib/glob/glob.go deleted file mode 100644 index c270dbc..0000000 --- a/lib/glob/glob.go +++ /dev/null @@ -1,108 +0,0 @@ -// Package glob provides objects for matching strings with globs -package glob - -import "regexp" - -// Glob is a wrapper of *regexp.Regexp. -// It should contain a glob expression compiled into a regular expression. -type Glob struct { - *regexp.Regexp -} - -// Compile a takes a glob expression as a string and transforms it -// into a *Glob object (which is really just a regular expression) -// Compile also returns a possible error. -func Compile(pattern string) (*Glob, error) { - r, err := globToRegex(pattern) - return &Glob{r}, err -} - -func globToRegex(glob string) (*regexp.Regexp, error) { - regex := "" - inGroup := 0 - inClass := 0 - firstIndexInClass := -1 - arr := []byte(glob) - - hasGlobCharacters := false - - for i := 0; i < len(arr); i++ { - ch := arr[i] - - switch ch { - case '\\': - i++ - if i >= len(arr) { - regex += "\\" - } else { - next := arr[i] - switch next { - case ',': - // Nothing - case 'Q', 'E': - regex += "\\\\" - default: - regex += "\\" - } - regex += string(next) - } - case '*': - if inClass == 0 { - regex += ".*" - } else { - regex += "*" - } - hasGlobCharacters = true - case '?': - if inClass == 0 { - regex += "." - } else { - regex += "?" - } - hasGlobCharacters = true - case '[': - inClass++ - firstIndexInClass = i + 1 - regex += "[" - hasGlobCharacters = true - case ']': - inClass-- - regex += "]" - case '.', '(', ')', '+', '|', '^', '$', '@', '%': - if inClass == 0 || (firstIndexInClass == i && ch == '^') { - regex += "\\" - } - regex += string(ch) - hasGlobCharacters = true - case '!': - if firstIndexInClass == i { - regex += "^" - } else { - regex += "!" - } - hasGlobCharacters = true - case '{': - inGroup++ - regex += "(" - hasGlobCharacters = true - case '}': - inGroup-- - regex += ")" - case ',': - if inGroup > 0 { - regex += "|" - hasGlobCharacters = true - } else { - regex += "," - } - default: - regex += string(ch) - } - } - - if hasGlobCharacters { - return regexp.Compile("^" + regex + "$") - } else { - return regexp.Compile(regex) - } -} diff --git a/matrix/history.go b/matrix/history.go index bb480f0..8a80569 100644 --- a/matrix/history.go +++ b/matrix/history.go @@ -26,9 +26,10 @@ import ( sync "github.com/sasha-s/go-deadlock" bolt "go.etcd.io/bbolt" - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" ) type HistoryManager struct { @@ -87,7 +88,7 @@ func (hm *HistoryManager) Close() error { var ( EventNotFoundError = errors.New("event not found") - RoomNotFoundError = errors.New("room not found") + RoomNotFoundError = errors.New("room not found") ) func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []byte) (*bolt.Bucket, []byte, error) { @@ -103,7 +104,7 @@ func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []b return stream, index, nil } -func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*event.Event, error) { +func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*muksevt.Event, error) { eventData := stream.Get(index) if eventData == nil || len(eventData) == 0 { return nil, EventNotFoundError @@ -111,7 +112,7 @@ func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byt return unmarshalEvent(eventData) } -func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Event, err error) { +func (hm *HistoryManager) Get(room *rooms.Room, eventID id.EventID) (evt *muksevt.Event, err error) { err = hm.db.View(func(tx *bolt.Tx) error { if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil { return err @@ -123,7 +124,7 @@ func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Even return } -func (hm *HistoryManager) Update(room *rooms.Room, eventID string, update func(evt *event.Event) error) error { +func (hm *HistoryManager) Update(room *rooms.Room, eventID id.EventID, update func(evt *muksevt.Event) error) error { return hm.db.Update(func(tx *bolt.Tx) error { if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil { return err @@ -140,18 +141,18 @@ func (hm *HistoryManager) Update(room *rooms.Room, eventID string, update func(e }) } -func (hm *HistoryManager) Append(room *rooms.Room, events []*mautrix.Event) ([]*event.Event, error) { +func (hm *HistoryManager) Append(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) { return hm.store(room, events, true) } -func (hm *HistoryManager) Prepend(room *rooms.Room, events []*mautrix.Event) ([]*event.Event, error) { +func (hm *HistoryManager) Prepend(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) { return hm.store(room, events, false) } -func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, append bool) ([]*event.Event, error) { +func (hm *HistoryManager) store(room *rooms.Room, events []*event.Event, append bool) ([]*muksevt.Event, error) { hm.Lock() defer hm.Unlock() - newEvents := make([]*event.Event, len(events)) + newEvents := make([]*muksevt.Event, len(events)) err := hm.db.Update(func(tx *bolt.Tx) error { streamPointers := tx.Bucket(bucketStreamPointers) rid := []byte(room.ID) @@ -177,7 +178,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen return err } for i, evt := range events { - newEvents[i] = event.Wrap(evt) + newEvents[i] = muksevt.Wrap(evt) if err := put(stream, eventIDs, newEvents[i], ptrStart+uint64(i)); err != nil { return err } @@ -198,7 +199,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen } eventCount := uint64(len(events)) for i, evt := range events { - newEvents[i] = event.Wrap(evt) + newEvents[i] = muksevt.Wrap(evt) if err := put(stream, eventIDs, newEvents[i], -ptrStart-uint64(i)); err != nil { return err } @@ -215,12 +216,11 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen return newEvents, err } -func (hm *HistoryManager) Load(room *rooms.Room, num int) (events []*event.Event, err error) { +func (hm *HistoryManager) Load(room *rooms.Room, num int) (events []*muksevt.Event, err error) { hm.Lock() defer hm.Unlock() err = hm.db.View(func(tx *bolt.Tx) error { - rid := []byte(room.ID) - stream := tx.Bucket(bucketRoomStreams).Bucket(rid) + stream := tx.Bucket(bucketRoomStreams).Bucket([]byte(room.ID)) if stream == nil { return nil } @@ -265,7 +265,7 @@ func btoi(b []byte) uint64 { return binary.BigEndian.Uint64(b) } -func marshalEvent(evt *event.Event) ([]byte, error) { +func marshalEvent(evt *muksevt.Event) ([]byte, error) { var buf bytes.Buffer enc := gzip.NewWriter(&buf) if err := gob.NewEncoder(enc).Encode(evt); err != nil { @@ -277,8 +277,8 @@ func marshalEvent(evt *event.Event) ([]byte, error) { return buf.Bytes(), nil } -func unmarshalEvent(data []byte) (*event.Event, error) { - evt := &event.Event{} +func unmarshalEvent(data []byte) (*muksevt.Event, error) { + evt := &muksevt.Event{} if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil { return nil, err } else if err := gob.NewDecoder(cmpReader).Decode(evt); err != nil { @@ -290,7 +290,7 @@ func unmarshalEvent(data []byte) (*event.Event, error) { return evt, nil } -func put(streams, eventIDs *bolt.Bucket, evt *event.Event, key uint64) error { +func put(streams, eventIDs *bolt.Bucket, evt *muksevt.Event, key uint64) error { data, err := marshalEvent(evt) if err != nil { return err diff --git a/matrix/matrix.go b/matrix/matrix.go index 651f6bb..cd40a5a 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -36,15 +36,17 @@ import ( "github.com/pkg/errors" "maunium.net/go/gomuks/lib/open" - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/mautrix/pushrules" ) // Container is a wrapper for a mautrix Client and some other stuff. @@ -96,7 +98,8 @@ func (c *Container) InitClient() error { c.client = nil } - var mxid, accessToken string + var mxid id.UserID + var accessToken string if len(c.config.AccessToken) > 0 { accessToken = c.config.AccessToken mxid = c.config.UserID @@ -180,7 +183,7 @@ func respondHTML(w http.ResponseWriter, status int, message string) { } func (c *Container) SingleSignOn() error { - loginURL := c.client.BuildURLWithQuery([]string{"login", "sso", "redirect"}, map[string]string{ + loginURL := c.client.BuildURLWithQuery(mautrix.URLPath{"login", "sso", "redirect"}, map[string]string{ "redirectUrl": "http://localhost:29325", }) err := open.Open(loginURL) @@ -267,7 +270,7 @@ func (c *Container) Stop() { // 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 := pushrules.GetPushRules(c.client) + resp, err := c.client.GetPushRules() if err != nil { debug.Print("Failed to fetch push rules:", err) c.config.PushRules = &pushrules.PushRuleset{} @@ -285,7 +288,10 @@ func (c *Container) PushRules() *pushrules.PushRuleset { return c.config.PushRules } -var AccountDataGomuksPreferences = mautrix.NewEventType("net.maunium.gomuks.preferences") +var AccountDataGomuksPreferences = event.Type{ + Type: "net.maunium.gomuks.preferences", + Class: event.AccountDataEventType, +} // OnLogin initializes the syncer and updates the room list. func (c *Container) OnLogin() { @@ -295,21 +301,21 @@ func (c *Container) OnLogin() { debug.Print("Initializing syncer") c.syncer = NewGomuksSyncer(c.config) - c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventReaction, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction) - c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateRoomName, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership) - c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt) - c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping) - c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo) - c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules) - c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag) + c.syncer.OnEventType(event.EventMessage, c.HandleMessage) + c.syncer.OnEventType(event.EventEncrypted, c.HandleMessage) + c.syncer.OnEventType(event.EventSticker, c.HandleMessage) + c.syncer.OnEventType(event.EventReaction, c.HandleMessage) + c.syncer.OnEventType(event.EventRedaction, c.HandleRedaction) + c.syncer.OnEventType(event.StateAliases, c.HandleMessage) + c.syncer.OnEventType(event.StateCanonicalAlias, c.HandleMessage) + c.syncer.OnEventType(event.StateTopic, c.HandleMessage) + c.syncer.OnEventType(event.StateRoomName, c.HandleMessage) + c.syncer.OnEventType(event.StateMember, c.HandleMembership) + c.syncer.OnEventType(event.EphemeralEventReceipt, c.HandleReadReceipt) + c.syncer.OnEventType(event.EphemeralEventTyping, c.HandleTyping) + c.syncer.OnEventType(event.AccountDataDirectChats, c.HandleDirectChatInfo) + c.syncer.OnEventType(event.AccountDataPushRules, c.HandlePushRules) + c.syncer.OnEventType(event.AccountDataRoomTags, c.HandleTag) c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences) c.syncer.InitDoneCallback = func() { debug.Print("Initial sync done") @@ -372,7 +378,7 @@ func (c *Container) Start() { } } -func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) { +func (c *Container) HandlePreferences(source EventSource, evt *event.Event) { if source&EventSourceAccountData == 0 { return } @@ -395,18 +401,17 @@ func (c *Container) Preferences() *config.UserPreferences { func (c *Container) SendPreferencesToMatrix() { defer debug.Recover() debug.Print("Sending updated preferences:", c.config.Preferences) - u := c.client.BuildURL("user", c.config.UserID, "account_data", AccountDataGomuksPreferences.Type) + u := c.client.BuildURL("user", string(c.config.UserID), "account_data", AccountDataGomuksPreferences.Type) _, err := c.client.MakeRequest("PUT", u, &c.config.Preferences, nil) if err != nil { debug.Print("Failed to update preferences:", err) } } -func (c *Container) HandleRedaction(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleRedaction(source EventSource, evt *event.Event) { room := c.GetOrCreateRoom(evt.RoomID) - var redactedEvt *event.Event - err := c.history.Update(room, evt.Redacts, func(redacted *event.Event) error { - redacted.Unsigned.RedactedBy = evt.ID + var redactedEvt *muksevt.Event + err := c.history.Update(room, evt.Redacts, func(redacted *muksevt.Event) error { redacted.Unsigned.RedactedBecause = evt redactedEvt = redacted return nil @@ -430,9 +435,9 @@ func (c *Container) HandleRedaction(source EventSource, evt *mautrix.Event) { } } -func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *event.Event) { - var origEvt *event.Event - err := c.history.Update(room, editsID, func(evt *event.Event) error { +func (c *Container) HandleEdit(room *rooms.Room, editsID id.EventID, editEvent *muksevt.Event) { + var origEvt *muksevt.Event + err := c.history.Update(room, editsID, func(evt *muksevt.Event) error { evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent) origEvt = evt return nil @@ -456,10 +461,10 @@ func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *even } } -func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent *event.Event) { +func (c *Container) HandleReaction(room *rooms.Room, reactsTo id.EventID, reactEvent *muksevt.Event) { rel := reactEvent.Content.GetRelatesTo() - var origEvt *event.Event - err := c.history.Update(room, reactsTo, func(evt *event.Event) error { + var origEvt *muksevt.Event + err := c.history.Update(room, reactsTo, func(evt *muksevt.Event) error { if evt.Unsigned.Relations.Annotations.Map == nil { evt.Unsigned.Relations.Annotations.Map = make(map[string]int) } @@ -488,7 +493,7 @@ func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent } // HandleMessage is the event handler for the m.room.message timeline event. -func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { +func (c *Container) HandleMessage(source EventSource, mxEvent *event.Event) { room := c.GetOrCreateRoom(mxEvent.RoomID) if source&EventSourceLeave != 0 { room.HasLeft = true @@ -498,14 +503,14 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { } if editID := mxEvent.Content.GetRelatesTo().GetReplaceID(); len(editID) > 0 { - c.HandleEdit(room, editID, event.Wrap(mxEvent)) + c.HandleEdit(room, editID, muksevt.Wrap(mxEvent)) return - } else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == mautrix.EventReaction && len(reactionID) > 0 { - c.HandleReaction(room, reactionID, event.Wrap(mxEvent)) + } else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == event.EventReaction && len(reactionID) > 0 { + c.HandleReaction(room, reactionID, muksevt.Wrap(mxEvent)) return } - events, err := c.history.Append(room, []*mautrix.Event{mxEvent}) + events, err := c.history.Append(room, []*event.Event{mxEvent}) if err != nil { debug.Printf("Failed to add event %s to history: %v", mxEvent.ID, err) } @@ -549,7 +554,7 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { } // HandleMembership is the event handler for the m.room.member state event. -func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleMembership(source EventSource, evt *event.Event) { isLeave := source&EventSourceLeave != 0 isTimeline := source&EventSourceTimeline != 0 if isLeave { @@ -558,7 +563,7 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { isNonTimelineLeave := isLeave && !isTimeline if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { return - } else if evt.StateKey != nil && *evt.StateKey == c.config.UserID { + } else if evt.StateKey != nil && id.UserID(*evt.StateKey) == c.config.UserID { c.processOwnMembershipChange(evt) } else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) { // We don't care about other users' membership events in the initial sync or chats we've left. @@ -568,9 +573,9 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { c.HandleMessage(source, evt) } -func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { +func (c *Container) processOwnMembershipChange(evt *event.Event) { membership := evt.Content.Membership - prevMembership := mautrix.MembershipLeave + prevMembership := event.MembershipLeave if evt.Unsigned.PrevContent != nil { prevMembership = evt.Unsigned.PrevContent.Membership } @@ -603,7 +608,7 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { c.ui.Render() } -func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent string) { +func (c *Container) parseReadReceipt(evt *event.Event) (largestTimestampEvent id.EventID) { var largestTimestamp int64 for eventID, rawContent := range evt.Content.Raw { content, ok := rawContent.(map[string]interface{}) @@ -616,7 +621,7 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent continue } - myInfo, ok := mRead[c.config.UserID].(map[string]interface{}) + myInfo, ok := mRead[string(c.config.UserID)].(map[string]interface{}) if !ok { continue } @@ -624,13 +629,13 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent ts, ok := myInfo["ts"].(float64) if int64(ts) > largestTimestamp { largestTimestamp = int64(ts) - largestTimestampEvent = eventID + largestTimestampEvent = id.EventID(eventID) } } return } -func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleReadReceipt(source EventSource, evt *event.Event) { if source&EventSourceLeave != 0 { return } @@ -649,7 +654,7 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { } } -func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool { +func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]bool { directChats := make(map[*rooms.Room]bool) for _, rawRoomIDList := range evt.Content.Raw { roomIDList, ok := rawRoomIDList.([]interface{}) @@ -663,7 +668,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool continue } - room := c.GetOrCreateRoom(roomID) + room := c.GetOrCreateRoom(id.RoomID(roomID)) if room != nil && !room.HasLeft { directChats[room] = true } @@ -672,7 +677,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool return directChats } -func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandleDirectChatInfo(_ EventSource, evt *event.Event) { directChats := c.parseDirectChatInfo(evt) for _, room := range c.config.Rooms.Map { shouldBeDirect := directChats[room] @@ -686,7 +691,7 @@ func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) { } // HandlePushRules is the event handler for the m.push_rules account data event. -func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandlePushRules(_ EventSource, evt *event.Event) { debug.Print("Received updated push rules") var err error c.config.PushRules, err = pushrules.EventToPushRules(evt) @@ -698,7 +703,7 @@ func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) { } // HandleTag is the event handler for the m.tag account data event. -func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandleTag(_ EventSource, evt *event.Event) { debug.Printf("Received tags for %s: %s -- %s", evt.RoomID, evt.Content.RoomTags, string(evt.Content.VeryRaw)) room := c.GetOrCreateRoom(evt.RoomID) @@ -724,24 +729,24 @@ func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) { } // HandleTyping is the event handler for the m.typing event. -func (c *Container) HandleTyping(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandleTyping(_ EventSource, evt *event.Event) { if !c.config.AuthCache.InitialSyncDone { return } c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs) } -func (c *Container) MarkRead(roomID, eventID string) { +func (c *Container) MarkRead(roomID id.RoomID, eventID id.EventID) { urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID) _, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil) } -func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, rel *ifc.Relation) *event.Event { - var content mautrix.Content +func (c *Container) PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, rel *ifc.Relation) *muksevt.Event { + var content event.Content if html != "" { - content = mautrix.Content{ + content = event.Content{ FormattedBody: html, - Format: mautrix.FormatHTML, + Format: event.FormatHTML, Body: text, MsgType: msgtype, } @@ -750,49 +755,49 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag content.MsgType = msgtype } - if rel != nil && rel.Type == mautrix.RelReplace { + if rel != nil && rel.Type == event.RelReplace { contentCopy := content content.NewContent = &contentCopy content.Body = "* " + content.Body if len(content.FormattedBody) > 0 { content.FormattedBody = "* " + content.FormattedBody } - content.RelatesTo = &mautrix.RelatesTo{ - Type: mautrix.RelReplace, + content.RelatesTo = &event.RelatesTo{ + Type: event.RelReplace, EventID: rel.Event.ID, } - } else if rel != nil && rel.Type == mautrix.RelReference { + } else if rel != nil && rel.Type == event.RelReference { content.SetReply(rel.Event.Event) } txnID := c.client.TxnID() - localEcho := event.Wrap(&mautrix.Event{ - ID: txnID, + localEcho := muksevt.Wrap(&event.Event{ + ID: id.EventID(txnID), Sender: c.config.UserID, - Type: mautrix.EventMessage, + Type: event.EventMessage, Timestamp: time.Now().UnixNano() / 1e6, RoomID: roomID, Content: content, - Unsigned: mautrix.Unsigned{ + Unsigned: event.Unsigned{ TransactionID: txnID, }, }) - localEcho.Gomuks.OutgoingState = event.StateLocalEcho - if rel != nil && rel.Type == mautrix.RelReplace { + localEcho.Gomuks.OutgoingState = muksevt.StateLocalEcho + if rel != nil && rel.Type == event.RelReplace { localEcho.ID = rel.Event.ID - localEcho.Gomuks.Edits = []*event.Event{localEcho} + localEcho.Gomuks.Edits = []*muksevt.Event{localEcho} } return localEcho } -func (c *Container) Redact(roomID, eventID, reason string) error { +func (c *Container) Redact(roomID id.RoomID, eventID id.EventID, reason string) error { defer debug.Recover() _, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason}) return err } // SendMessage sends the given event. -func (c *Container) SendEvent(event *event.Event) (string, error) { +func (c *Container) SendEvent(event *muksevt.Event) (id.EventID, error) { defer debug.Recover() c.client.UserTyping(event.RoomID, false, 0) @@ -804,13 +809,13 @@ func (c *Container) SendEvent(event *event.Event) (string, error) { return resp.EventID, nil } -func (c *Container) sendTypingAsync(roomID string, typing bool, timeout int64) { +func (c *Container) sendTypingAsync(roomID id.RoomID, typing bool, timeout int64) { defer debug.Recover() _, _ = c.client.UserTyping(roomID, typing, timeout) } // SendTyping sets whether or not the user is typing in the given room. -func (c *Container) SendTyping(roomID string, typing bool) { +func (c *Container) SendTyping(roomID id.RoomID, typing bool) { ts := time.Now().Unix() if (c.typing > ts && typing) || (c.typing == 0 && !typing) { return @@ -836,8 +841,8 @@ func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) } // JoinRoom makes the current user try to join the given room. -func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) { - resp, err := c.client.JoinRoom(roomID, server, nil) +func (c *Container) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) { + resp, err := c.client.JoinRoom(string(roomID), server, nil) if err != nil { return nil, err } @@ -848,7 +853,7 @@ func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) { } // LeaveRoom makes the current user leave the given room. -func (c *Container) LeaveRoom(roomID string) error { +func (c *Container) LeaveRoom(roomID id.RoomID) error { _, err := c.client.LeaveRoom(roomID) if err != nil { return err @@ -873,7 +878,7 @@ func (c *Container) FetchMembers(room *rooms.Room) error { } // GetHistory fetches room history. -func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, error) { +func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*muksevt.Event, error) { events, err := c.history.Load(room, limit) if err != nil { return nil, err @@ -893,7 +898,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err room.PrevBatch = resp.End c.config.Rooms.Put(room) if len(resp.Chunk) == 0 { - return []*event.Event{}, nil + return []*muksevt.Event{}, nil } events, err = c.history.Prepend(room, resp.Chunk) if err != nil { @@ -902,7 +907,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err return events, nil } -func (c *Container) GetEvent(room *rooms.Room, eventID string) (*event.Event, error) { +func (c *Container) GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error) { evt, err := c.history.Get(room, eventID) if err != nil && err != EventNotFoundError { debug.Printf("Failed to get event %s from local cache: %v", eventID, err) @@ -914,18 +919,18 @@ func (c *Container) GetEvent(room *rooms.Room, eventID string) (*event.Event, er if err != nil { return nil, err } - evt = event.Wrap(mxEvent) + evt = muksevt.Wrap(mxEvent) debug.Printf("Loaded event %s from server", eventID) return evt, nil } // GetOrCreateRoom gets the room instance stored in the session. -func (c *Container) GetOrCreateRoom(roomID string) *rooms.Room { +func (c *Container) GetOrCreateRoom(roomID id.RoomID) *rooms.Room { return c.config.Rooms.GetOrCreate(roomID) } // GetRoom gets the room instance stored in the session. -func (c *Container) GetRoom(roomID string) *rooms.Room { +func (c *Container) GetRoom(roomID id.RoomID) *rooms.Room { return c.config.Rooms.Get(roomID) } @@ -949,7 +954,7 @@ func cp(src, dst string) error { return out.Close() } -func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullPath string, err error) { +func (c *Container) DownloadToDisk(uri id.ContentURI, target string) (fullPath string, err error) { cachePath := c.GetCachePath(uri) if target == "" { fullPath = cachePath @@ -994,7 +999,7 @@ func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullP // Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors. // // The file will be either read from the media cache (if found) or downloaded from the server. -func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) { +func (c *Container) Download(uri id.ContentURI) (data []byte, err error) { cacheFile := c.GetCachePath(uri) var info os.FileInfo if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() { @@ -1008,7 +1013,7 @@ func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) { return } -func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string { +func (c *Container) GetDownloadURL(uri id.ContentURI) string { dlURL, _ := url.Parse(c.client.HomeserverURL.String()) if dlURL.Scheme == "" { dlURL.Scheme = "https" @@ -1017,7 +1022,7 @@ func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string { return dlURL.String() } -func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []byte, err error) { +func (c *Container) download(uri id.ContentURI, cacheFile string) (data []byte, err error) { var resp *http.Response resp, err = c.client.Client.Get(c.GetDownloadURL(uri)) if err != nil { @@ -1039,7 +1044,7 @@ func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []b // GetCachePath gets the path to the cached version of the given homeserver:fileID combination. // The file may or may not exist, use Download() to ensure it has been cached. -func (c *Container) GetCachePath(uri mautrix.ContentURI) string { +func (c *Container) GetCachePath(uri id.ContentURI) string { dir := filepath.Join(c.config.MediaDir, uri.Homeserver) err := os.MkdirAll(dir, 0700) diff --git a/matrix/matrix_test.go b/matrix/matrix_test.go deleted file mode 100644 index 05362b9..0000000 --- a/matrix/matrix_test.go +++ /dev/null @@ -1,232 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package matrix - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - - "maunium.net/go/gomuks/config" - "maunium.net/go/mautrix" -) - -func TestContainer_InitClient_Empty(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-0") - os.MkdirAll("/tmp/gomuks-mxtest-0", 0700) - cfg := config.NewConfig("/tmp/gomuks-mxtest-0", "/tmp/gomuks-mxtest-0") - cfg.HS = "https://matrix.org" - c := Container{config: cfg} - assert.Nil(t, c.InitClient()) -} - -func TestContainer_GetCachePath(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-1") - cfg := config.NewConfig("/tmp/gomuks-mxtest-1", "/tmp/gomuks-mxtest-1") - c := Container{config: cfg} - assert.Equal(t, "/tmp/gomuks-mxtest-1/media/maunium.net/foobar", c.GetCachePath("maunium.net", "foobar")) -} - -/* FIXME probably not applicable anymore -func TestContainer_SendMarkdownMessage_NoMarkdown(t *testing.T) { - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPut || !strings.HasPrefix(req.URL.Path, "/_matrix/client/r0/rooms/!foo:example.com/send/m.room.message/") { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - - body := parseBody(req) - assert.Equal(t, "m.text", body["msgtype"]) - assert.Equal(t, "test message", body["body"]) - return mockResponse(http.StatusOK, `{"event_id": "!foobar1:example.com"}`), nil - })} - - event := c.PrepareMarkdownMessage("!foo:example.com", "m.text", "test message") - evtID, err := c.SendEvent(event) - assert.Nil(t, err) - assert.Equal(t, "!foobar1:example.com", evtID) -}*/ - -func TestContainer_SendMarkdownMessage_WithMarkdown(t *testing.T) { - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPut || !strings.HasPrefix(req.URL.Path, "/_matrix/client/r0/rooms/!foo:example.com/send/m.room.message/") { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - - body := parseBody(req) - assert.Equal(t, "m.text", body["msgtype"]) - assert.Equal(t, "**formatted** test _message_", body["body"]) - assert.Equal(t, "<p><strong>formatted</strong> <u>test</u> <em>message</em></p>\n", body["formatted_body"]) - return mockResponse(http.StatusOK, `{"event_id": "!foobar2:example.com"}`), nil - }), config: &config.Config{UserID: "@user:example.com"}} - - event := c.PrepareMarkdownMessage("!foo:example.com", "m.text", "**formatted** <u>test</u> _message_") - evtID, err := c.SendEvent(event) - assert.Nil(t, err) - assert.Equal(t, "!foobar2:example.com", evtID) -} - -func TestContainer_SendTyping(t *testing.T) { - var calls []mautrix.ReqTyping - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPut || req.URL.Path != "/_matrix/client/r0/rooms/!foo:example.com/typing/@user:example.com" { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - - rawBody, err := ioutil.ReadAll(req.Body) - if err != nil { - return nil, err - } - - call := mautrix.ReqTyping{} - err = json.Unmarshal(rawBody, &call) - if err != nil { - return nil, err - } - calls = append(calls, call) - - return mockResponse(http.StatusOK, `{}`), nil - })} - - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", false) - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", false) - assert.Len(t, calls, 4) - assert.True(t, calls[0].Typing) - assert.False(t, calls[1].Typing) - assert.True(t, calls[2].Typing) - assert.False(t, calls[3].Typing) -} - -func TestContainer_JoinRoom(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-2") - cfg := config.NewConfig("/tmp/gomuks-mxtest-2", "/tmp/gomuks-mxtest-2") - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method == http.MethodPost && req.URL.Path == "/_matrix/client/r0/join/!foo:example.com" { - return mockResponse(http.StatusOK, `{"room_id": "!foo:example.com"}`), nil - } else if req.Method == http.MethodPost && req.URL.Path == "/_matrix/client/r0/rooms/!foo:example.com/leave" { - return mockResponse(http.StatusOK, `{}`), nil - } - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - }), config: cfg} - - room, err := c.JoinRoom("!foo:example.com", "") - assert.Nil(t, err) - assert.Equal(t, "!foo:example.com", room.ID) - assert.False(t, room.HasLeft) - - err = c.LeaveRoom("!foo:example.com") - assert.Nil(t, err) - assert.True(t, room.HasLeft) -} - -func TestContainer_Download(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-3") - cfg := config.NewConfig("/tmp/gomuks-mxtest-3", "/tmp/gomuks-mxtest-3") - cfg.LoadAll() - callCounter := 0 - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodGet || req.URL.Path != "/_matrix/media/v1/download/example.com/foobar" { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - callCounter++ - return mockResponse(http.StatusOK, `example file`), nil - }), config: cfg} - - // Check that download works - data, hs, id, err := c.Download("mxc://example.com/foobar") - assert.Equal(t, "example.com", hs) - assert.Equal(t, "foobar", id) - assert.Equal(t, 1, callCounter) - assert.Equal(t, []byte("example file"), data) - assert.Nil(t, err) - - // Check that cache works - data, _, _, err = c.Download("mxc://example.com/foobar") - assert.Nil(t, err) - assert.Equal(t, []byte("example file"), data) - assert.Equal(t, 1, callCounter) -} - -func TestContainer_Download_InvalidURL(t *testing.T) { - c := Container{} - data, hs, id, err := c.Download("mxc://invalid mxc") - assert.NotNil(t, err) - assert.Empty(t, id) - assert.Empty(t, hs) - assert.Empty(t, data) -} - -/* FIXME -func TestContainer_GetHistory(t *testing.T) { - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodGet || req.URL.Path != "/_matrix/client/r0/rooms/!foo:maunium.net/messages" { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - return mockResponse(http.StatusOK, `{"start": "123", "end": "456", "chunk": [{"event_id": "it works"}]}`), nil - })} - - history, prevBatch, err := c.GetHistory("!foo:maunium.net", "123", 5) - assert.Nil(t, err) - assert.Equal(t, "it works", history[0].ID) - assert.Equal(t, "456", prevBatch) -}*/ - -func mockClient(fn func(*http.Request) (*http.Response, error)) *mautrix.Client { - client, _ := mautrix.NewClient("https://example.com", "@user:example.com", "foobar") - client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}} - return client -} - -func parseBody(req *http.Request) map[string]interface{} { - rawBody, err := ioutil.ReadAll(req.Body) - if err != nil { - panic(err) - } - - data := make(map[string]interface{}) - - err = json.Unmarshal(rawBody, &data) - if err != nil { - panic(err) - } - - return data -} - -func mockResponse(status int, body string) *http.Response { - return &http.Response{ - StatusCode: status, - Body: ioutil.NopCloser(strings.NewReader(body)), - } -} - -type MockRoundTripper struct { - RT func(*http.Request) (*http.Response, error) -} - -func (t MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return t.RT(req) -} diff --git a/matrix/event/event.go b/matrix/muksevt/event.go index 8506c9c..9f7a3ce 100644 --- a/matrix/event/event.go +++ b/matrix/muksevt/event.go @@ -14,14 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -package event +package muksevt import ( - "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" ) type Event struct { - *mautrix.Event + *event.Event Gomuks GomuksContent `json:"-"` } @@ -33,7 +33,7 @@ func (evt *Event) SomewhatDangerousCopy() *Event { } } -func Wrap(event *mautrix.Event) *Event { +func Wrap(event *event.Event) *Event { return &Event{Event: event} } diff --git a/matrix/pushrules/action.go b/matrix/pushrules/action.go deleted file mode 100644 index 4637950..0000000 --- a/matrix/pushrules/action.go +++ /dev/null @@ -1,134 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import "encoding/json" - -// PushActionType is the type of a PushAction -type PushActionType string - -// The allowed push action types as specified in spec section 11.12.1.4.1. -const ( - ActionNotify PushActionType = "notify" - ActionDontNotify PushActionType = "dont_notify" - ActionCoalesce PushActionType = "coalesce" - ActionSetTweak PushActionType = "set_tweak" -) - -// PushActionTweak is the type of the tweak in SetTweak push actions. -type PushActionTweak string - -// The allowed tweak types as specified in spec section 11.12.1.4.1.1. -const ( - TweakSound PushActionTweak = "sound" - TweakHighlight PushActionTweak = "highlight" -) - -// PushActionArray is an array of PushActions. -type PushActionArray []*PushAction - -// PushActionArrayShould contains the important information parsed from a PushActionArray. -type PushActionArrayShould struct { - // Whether or not the array contained a Notify, DontNotify or Coalesce action type. - NotifySpecified bool - // Whether or not the event in question should trigger a notification. - Notify bool - // Whether or not the event in question should be highlighted. - Highlight bool - - // Whether or not the event in question should trigger a sound alert. - PlaySound bool - // The name of the sound to play if PlaySound is true. - SoundName string -} - -// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct. -func (actions PushActionArray) Should() (should PushActionArrayShould) { - for _, action := range actions { - switch action.Action { - case ActionNotify, ActionCoalesce: - should.Notify = true - should.NotifySpecified = true - case ActionDontNotify: - should.Notify = false - should.NotifySpecified = true - case ActionSetTweak: - switch action.Tweak { - case TweakHighlight: - var ok bool - should.Highlight, ok = action.Value.(bool) - if !ok { - // Highlight value not specified, so assume true since the tweak is set. - should.Highlight = true - } - case TweakSound: - should.SoundName = action.Value.(string) - should.PlaySound = len(should.SoundName) > 0 - } - } - } - return -} - -// PushAction is a single action that should be triggered when receiving a message. -type PushAction struct { - Action PushActionType - Tweak PushActionTweak - Value interface{} -} - -// UnmarshalJSON parses JSON into this PushAction. -// -// * If the JSON is a single string, the value is stored in the Action field. -// * If the JSON is an object with the set_tweak field, Action will be set to -// "set_tweak", Tweak will be set to the value of the set_tweak field and -// and Value will be set to the value of the value field. -// * In any other case, the function does nothing. -func (action *PushAction) UnmarshalJSON(raw []byte) error { - var data interface{} - - err := json.Unmarshal(raw, &data) - if err != nil { - return err - } - - switch val := data.(type) { - case string: - action.Action = PushActionType(val) - case map[string]interface{}: - tweak, ok := val["set_tweak"].(string) - if ok { - action.Action = ActionSetTweak - action.Tweak = PushActionTweak(tweak) - action.Value, _ = val["value"] - } - } - return nil -} - -// MarshalJSON is the reverse of UnmarshalJSON() -func (action *PushAction) MarshalJSON() (raw []byte, err error) { - if action.Action == ActionSetTweak { - data := map[string]interface{}{ - "set_tweak": action.Tweak, - "value": action.Value, - } - return json.Marshal(&data) - } - data := string(action.Action) - return json.Marshal(&data) -} diff --git a/matrix/pushrules/action_test.go b/matrix/pushrules/action_test.go deleted file mode 100644 index 79b2fdf..0000000 --- a/matrix/pushrules/action_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/pushrules" -) - -func TestPushActionArray_Should_EmptyArrayReturnsDefaults(t *testing.T) { - should := pushrules.PushActionArray{}.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_MixedArrayReturnsExpected1(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.True(t, should.Notify) - assert.True(t, should.Highlight) - assert.True(t, should.PlaySound) - assert.Equal(t, "ping", should.SoundName) -} - -func TestPushActionArray_Should_MixedArrayReturnsExpected2(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: ""}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_NotifySet(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.True(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_NotifyAndCoalesceDoTheSameThing(t *testing.T) { - should1 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - }.Should() - should2 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - }.Should() - assert.Equal(t, should1, should2) -} - -func TestPushActionArray_Should_DontNotify(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_HighlightBlank(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.True(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_HighlightFalse(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_SoundName(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.True(t, should.PlaySound) - assert.Equal(t, "ping", should.SoundName) -} - -func TestPushActionArray_Should_SoundNameEmpty(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: ""}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushAction_UnmarshalJSON_InvalidJSONFails(t *testing.T) { - pa := &pushrules.PushAction{} - err := pa.UnmarshalJSON([]byte("Not JSON")) - assert.NotNil(t, err) -} - -func TestPushAction_UnmarshalJSON_InvalidTypeDoesNothing(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("unchanged"), - Tweak: pushrules.PushActionTweak("unchanged"), - Value: "unchanged", - } - - err := pa.UnmarshalJSON([]byte(`{"foo": "bar"}`)) - assert.Nil(t, err) - err = pa.UnmarshalJSON([]byte(`9001`)) - assert.Nil(t, err) - - assert.Equal(t, pushrules.PushActionType("unchanged"), pa.Action) - assert.Equal(t, pushrules.PushActionTweak("unchanged"), pa.Tweak) - assert.Equal(t, "unchanged", pa.Value) -} - -func TestPushAction_UnmarshalJSON_StringChangesActionType(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("unchanged"), - Tweak: pushrules.PushActionTweak("unchanged"), - Value: "unchanged", - } - - err := pa.UnmarshalJSON([]byte(`"foo"`)) - assert.Nil(t, err) - - assert.Equal(t, pushrules.PushActionType("foo"), pa.Action) - assert.Equal(t, pushrules.PushActionTweak("unchanged"), pa.Tweak) - assert.Equal(t, "unchanged", pa.Value) -} - -func TestPushAction_UnmarshalJSON_SetTweakChangesTweak(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("unchanged"), - Tweak: pushrules.PushActionTweak("unchanged"), - Value: "unchanged", - } - - err := pa.UnmarshalJSON([]byte(`{"set_tweak": "foo", "value": 123.0}`)) - assert.Nil(t, err) - - assert.Equal(t, pushrules.ActionSetTweak, pa.Action) - assert.Equal(t, pushrules.PushActionTweak("foo"), pa.Tweak) - assert.Equal(t, 123.0, pa.Value) -} - -func TestPushAction_MarshalJSON_TweakOutputWorks(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.ActionSetTweak, - Tweak: pushrules.PushActionTweak("foo"), - Value: "bar", - } - data, err := pa.MarshalJSON() - assert.Nil(t, err) - assert.Equal(t, []byte(`{"set_tweak":"foo","value":"bar"}`), data) -} - -func TestPushAction_MarshalJSON_OtherOutputWorks(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("something else"), - Tweak: pushrules.PushActionTweak("foo"), - Value: "bar", - } - data, err := pa.MarshalJSON() - assert.Nil(t, err) - assert.Equal(t, []byte(`"something else"`), data) -} diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go deleted file mode 100644 index cc62da1..0000000 --- a/matrix/pushrules/condition.go +++ /dev/null @@ -1,162 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import ( - "regexp" - "strconv" - "strings" - "unicode" - - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" - - "maunium.net/go/gomuks/lib/glob" -) - -// Room is an interface with the functions that are needed for processing room-specific push conditions -type Room interface { - GetMember(mxid string) *rooms.Member - GetMembers() map[string]*rooms.Member - GetSessionOwner() string -} - -// PushCondKind is the type of a push condition. -type PushCondKind string - -// The allowed push condition kinds as specified in section 11.12.1.4.3 of r0.3.0 of the Client-Server API. -const ( - KindEventMatch PushCondKind = "event_match" - KindContainsDisplayName PushCondKind = "contains_display_name" - KindRoomMemberCount PushCondKind = "room_member_count" -) - -// PushCondition wraps a condition that is required for a specific PushRule to be used. -type PushCondition struct { - // The type of the condition. - Kind PushCondKind `json:"kind"` - // The dot-separated field of the event to match. Only applicable if kind is EventMatch. - Key string `json:"key,omitempty"` - // The glob-style pattern to match the field against. Only applicable if kind is EventMatch. - Pattern string `json:"pattern,omitempty"` - // The condition that needs to be fulfilled for RoomMemberCount-type conditions. - // A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found. - MemberCountCondition string `json:"is,omitempty"` -} - -// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions. -var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") - -// Match checks if this condition is fulfilled for the given event in the given room. -func (cond *PushCondition) Match(room Room, event *mautrix.Event) bool { - switch cond.Kind { - case KindEventMatch: - return cond.matchValue(room, event) - case KindContainsDisplayName: - return cond.matchDisplayName(room, event) - case KindRoomMemberCount: - return cond.matchMemberCount(room, event) - default: - return false - } -} - -func (cond *PushCondition) matchValue(room Room, event *mautrix.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.String()) - 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.Raw[subkey].(string) - return pattern.MatchString(val) - default: - return false - } -} - -func (cond *PushCondition) matchDisplayName(room Room, event *mautrix.Event) bool { - ownerID := room.GetSessionOwner() - if ownerID == event.Sender { - return false - } - member := room.GetMember(ownerID) - if member == nil { - return false - } - - msg := event.Content.Body - isAcceptable := func(r uint8) bool { - return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r)) - } - length := len(member.Displayname) - for index := strings.Index(msg, member.Displayname); index != -1; index = strings.Index(msg, member.Displayname) { - if (index <= 0 || isAcceptable(msg[index-1])) && (index + length >= len(msg) || isAcceptable(msg[index+length])) { - return true - } - msg = msg[index+len(member.Displayname):] - } - return false -} - -func (cond *PushCondition) matchMemberCount(room Room, event *mautrix.Event) bool { - group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) - if len(group) != 3 { - return false - } - - operator := group[1] - wantedMemberCount, _ := strconv.Atoi(group[2]) - - memberCount := len(room.GetMembers()) - - switch operator { - case "==", "": - return memberCount == wantedMemberCount - case ">": - return memberCount > wantedMemberCount - case ">=": - return memberCount >= wantedMemberCount - case "<": - return memberCount < wantedMemberCount - case "<=": - return memberCount <= wantedMemberCount - default: - // Should be impossible due to regex. - return false - } -} diff --git a/matrix/pushrules/condition_displayname_test.go b/matrix/pushrules/condition_displayname_test.go deleted file mode 100644 index fd3f374..0000000 --- a/matrix/pushrules/condition_displayname_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "maunium.net/go/mautrix" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushCondition_Match_DisplayName(t *testing.T) { - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "tulir: test mention", - }) - event.Sender = "@someone_else:matrix.org" - assert.True(t, displaynamePushCondition.Match(displaynameTestRoom, event)) -} - -func TestPushCondition_Match_DisplayName_Fail(t *testing.T) { - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "not a mention", - }) - event.Sender = "@someone_else:matrix.org" - assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event)) -} - -func TestPushCondition_Match_DisplayName_CantHighlightSelf(t *testing.T) { - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "tulir: I can't highlight myself", - }) - assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event)) -} - -func TestPushCondition_Match_DisplayName_FailsOnEmptyRoom(t *testing.T) { - emptyRoom := newFakeRoom(0) - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "tulir: this room doesn't have the owner Member available, so it fails.", - }) - event.Sender = "@someone_else:matrix.org" - assert.False(t, displaynamePushCondition.Match(emptyRoom, event)) -} diff --git a/matrix/pushrules/condition_eventmatch_test.go b/matrix/pushrules/condition_eventmatch_test.go deleted file mode 100644 index e5761fc..0000000 --- a/matrix/pushrules/condition_eventmatch_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "maunium.net/go/mautrix" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushCondition_Match_KindEvent_MsgType(t *testing.T) { - condition := newMatchPushCondition("content.msgtype", "m.emote") - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.emote", - "body": "tests gomuks pushconditions", - }, - }) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_MsgType_Fail(t *testing.T) { - condition := newMatchPushCondition("content.msgtype", "m.emote") - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.text", - "body": "I'm testing gomuks pushconditions", - }, - }) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_EventType(t *testing.T) { - condition := newMatchPushCondition("type", "m.room.foo") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_EventType_IllegalGlob(t *testing.T) { - condition := newMatchPushCondition("type", "m.room.invalid_glo[b") - event := newFakeEvent(mautrix.NewEventType("m.room.invalid_glob"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_Sender_Fail(t *testing.T) { - condition := newMatchPushCondition("sender", "@foo:maunium.net") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_RoomID(t *testing.T) { - condition := newMatchPushCondition("room_id", "!fakeroom:maunium.net") - event := newFakeEvent(mautrix.NewEventType(""), mautrix.Content{}) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_BlankStateKey(t *testing.T) { - condition := newMatchPushCondition("state_key", "") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_BlankStateKey_Fail(t *testing.T) { - condition := newMatchPushCondition("state_key", "not blank") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_NonBlankStateKey(t *testing.T) { - condition := newMatchPushCondition("state_key", "*:maunium.net") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - event.StateKey = &event.Sender - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_UnknownKey(t *testing.T) { - condition := newMatchPushCondition("non-existent key", "doesn't affect anything") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} diff --git a/matrix/pushrules/condition_membercount_test.go b/matrix/pushrules/condition_membercount_test.go deleted file mode 100644 index ad5da9f..0000000 --- a/matrix/pushrules/condition_membercount_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushCondition_Match_KindMemberCount_OneToOne_ImplicitPrefix(t *testing.T) { - condition := newCountPushCondition("2") - room := newFakeRoom(2) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_OneToOne_ExplicitPrefix(t *testing.T) { - condition := newCountPushCondition("==2") - room := newFakeRoom(2) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_BigRoom(t *testing.T) { - condition := newCountPushCondition(">200") - room := newFakeRoom(201) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_BigRoom_Fail(t *testing.T) { - condition := newCountPushCondition(">=200") - room := newFakeRoom(199) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_SmallRoom(t *testing.T) { - condition := newCountPushCondition("<10") - room := newFakeRoom(9) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_SmallRoom_Fail(t *testing.T) { - condition := newCountPushCondition("<=10") - room := newFakeRoom(11) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_InvalidPrefix(t *testing.T) { - condition := newCountPushCondition("??10") - room := newFakeRoom(11) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_InvalidCondition(t *testing.T) { - condition := newCountPushCondition("foobar") - room := newFakeRoom(1) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} diff --git a/matrix/pushrules/condition_test.go b/matrix/pushrules/condition_test.go deleted file mode 100644 index 163c964..0000000 --- a/matrix/pushrules/condition_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/mautrix" - "maunium.net/go/gomuks/matrix/pushrules" - "maunium.net/go/gomuks/matrix/rooms" -) - -var ( - blankTestRoom *rooms.Room - displaynameTestRoom pushrules.Room - - countConditionTestEvent *mautrix.Event - - displaynamePushCondition *pushrules.PushCondition -) - -func init() { - blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net") - - countConditionTestEvent = &mautrix.Event{ - Sender: "@tulir:maunium.net", - Type: mautrix.EventMessage, - Timestamp: 1523791120, - ID: "$123:maunium.net", - RoomID: "!fakeroom:maunium.net", - Content: mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "test", - }, - } - - displaynameTestRoom = newFakeRoom(4) - displaynamePushCondition = &pushrules.PushCondition{ - Kind: pushrules.KindContainsDisplayName, - } -} - -func newFakeEvent(evtType mautrix.EventType, content mautrix.Content) *mautrix.Event { - return &mautrix.Event{ - Sender: "@tulir:maunium.net", - Type: evtType, - Timestamp: 1523791120, - ID: "$123:maunium.net", - RoomID: "!fakeroom:maunium.net", - Content: content, - } -} - -func newCountPushCondition(condition string) *pushrules.PushCondition { - return &pushrules.PushCondition{ - Kind: pushrules.KindRoomMemberCount, - MemberCountCondition: condition, - } -} - -func newMatchPushCondition(key, pattern string) *pushrules.PushCondition { - return &pushrules.PushCondition{ - Kind: pushrules.KindEventMatch, - Key: key, - Pattern: pattern, - } -} - -func TestPushCondition_Match_InvalidKind(t *testing.T) { - condition := &pushrules.PushCondition{ - Kind: pushrules.PushCondKind("invalid"), - } - event := newFakeEvent(mautrix.EventType{Type: "m.room.foobar"}, mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -type FakeRoom struct { - members map[string]*mautrix.Member - owner string -} - -func newFakeRoom(memberCount int) *FakeRoom { - room := &FakeRoom{ - owner: "@tulir:maunium.net", - members: make(map[string]*mautrix.Member), - } - - if memberCount >= 1 { - room.members["@tulir:maunium.net"] = &mautrix.Member{ - Membership: mautrix.MembershipJoin, - Displayname: "tulir", - } - } - - for i := 0; i < memberCount-1; i++ { - mxid := fmt.Sprintf("@extrauser_%d:matrix.org", i) - room.members[mxid] = &mautrix.Member{ - Membership: mautrix.MembershipJoin, - Displayname: fmt.Sprintf("Extra User %d", i), - } - } - - return room -} - -func (fr *FakeRoom) GetMember(mxid string) *mautrix.Member { - return fr.members[mxid] -} - -func (fr *FakeRoom) GetSessionOwner() string { - return fr.owner -} - -func (fr *FakeRoom) GetMembers() map[string]*mautrix.Member { - return fr.members -} diff --git a/matrix/pushrules/doc.go b/matrix/pushrules/doc.go deleted file mode 100644 index 19cd774..0000000 --- a/matrix/pushrules/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pushrules contains utilities to parse push notification rules. -package pushrules diff --git a/matrix/pushrules/pushrules.go b/matrix/pushrules/pushrules.go deleted file mode 100644 index 643f2f2..0000000 --- a/matrix/pushrules/pushrules.go +++ /dev/null @@ -1,37 +0,0 @@ -package pushrules - -import ( - "encoding/json" - "net/url" - - "maunium.net/go/mautrix" -) - -// GetPushRules returns the push notification rules for the global scope. -func GetPushRules(client *mautrix.Client) (*PushRuleset, error) { - return GetScopedPushRules(client, "global") -} - -// GetScopedPushRules returns the push notification rules for the given scope. -func GetScopedPushRules(client *mautrix.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 -} - -type contentWithRuleset struct { - Ruleset *PushRuleset `json:"global"` -} - -// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. -func EventToPushRules(event *mautrix.Event) (*PushRuleset, error) { - content := &contentWithRuleset{} - err := json.Unmarshal(event.Content.VeryRaw, content) - if err != nil { - return nil, err - } - - return content.Ruleset, nil -} diff --git a/matrix/pushrules/pushrules_test.go b/matrix/pushrules/pushrules_test.go deleted file mode 100644 index 1883c97..0000000 --- a/matrix/pushrules/pushrules_test.go +++ /dev/null @@ -1,249 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/mautrix" - "maunium.net/go/gomuks/matrix/pushrules" -) - -func TestEventToPushRules(t *testing.T) { - event := &mautrix.Event{ - Type: mautrix.AccountDataPushRules, - Timestamp: 1523380910, - Content: mautrix.Content{ - VeryRaw: json.RawMessage(JSONExamplePushRules), - }, - } - pushRuleset, err := pushrules.EventToPushRules(event) - assert.Nil(t, err) - assert.NotNil(t, pushRuleset) - - assert.IsType(t, pushRuleset.Override, pushrules.PushRuleArray{}) - assert.IsType(t, pushRuleset.Content, pushrules.PushRuleArray{}) - assert.IsType(t, pushRuleset.Room, pushrules.PushRuleMap{}) - assert.IsType(t, pushRuleset.Sender, pushrules.PushRuleMap{}) - assert.IsType(t, pushRuleset.Underride, pushrules.PushRuleArray{}) - assert.Len(t, pushRuleset.Override, 2) - assert.Len(t, pushRuleset.Content, 1) - assert.Empty(t, pushRuleset.Room.Map) - assert.Empty(t, pushRuleset.Sender.Map) - assert.Len(t, pushRuleset.Underride, 6) - - assert.Len(t, pushRuleset.Content[0].Actions, 3) - assert.True(t, pushRuleset.Content[0].Default) - assert.True(t, pushRuleset.Content[0].Enabled) - assert.Empty(t, pushRuleset.Content[0].Conditions) - assert.Equal(t, "alice", pushRuleset.Content[0].Pattern) - assert.Equal(t, ".m.rule.contains_user_name", pushRuleset.Content[0].RuleID) - - assert.False(t, pushRuleset.Override[0].Actions.Should().Notify) - assert.True(t, pushRuleset.Override[0].Actions.Should().NotifySpecified) -} - -const JSONExamplePushRules = `{ - "global": { - "content": [ - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight" - } - ], - "default": true, - "enabled": true, - "pattern": "alice", - "rule_id": ".m.rule.contains_user_name" - } - ], - "override": [ - { - "actions": [ - "dont_notify" - ], - "conditions": [], - "default": true, - "enabled": false, - "rule_id": ".m.rule.master" - }, - { - "actions": [ - "dont_notify" - ], - "conditions": [ - { - "key": "content.msgtype", - "kind": "event_match", - "pattern": "m.notice" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.suppress_notices" - } - ], - "room": [], - "sender": [], - "underride": [ - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "ring" - }, - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.call.invite" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.call" - }, - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight" - } - ], - "conditions": [ - { - "kind": "contains_display_name" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.contains_display_name" - }, - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "is": "2", - "kind": "room_member_count" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.room_one_to_one" - }, - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.room.member" - }, - { - "key": "content.membership", - "kind": "event_match", - "pattern": "invite" - }, - { - "key": "state_key", - "kind": "event_match", - "pattern": "@alice:example.com" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.invite_for_me" - }, - { - "actions": [ - "notify", - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.room.member" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.member_event" - }, - { - "actions": [ - "notify", - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.room.message" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.message" - } - ] - } -}` diff --git a/matrix/pushrules/rule.go b/matrix/pushrules/rule.go deleted file mode 100644 index ef43721..0000000 --- a/matrix/pushrules/rule.go +++ /dev/null @@ -1,160 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import ( - "encoding/gob" - - "maunium.net/go/mautrix" - - "maunium.net/go/gomuks/lib/glob" -) - -func init() { - gob.Register(PushRuleArray{}) - gob.Register(PushRuleMap{}) -} - -type PushRuleCollection interface { - GetActions(room Room, event *mautrix.Event) PushActionArray -} - -type PushRuleArray []*PushRule - -func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray { - for _, rule := range rules { - rule.Type = typ - } - return rules -} - -func (rules PushRuleArray) GetActions(room Room, event *mautrix.Event) PushActionArray { - for _, rule := range rules { - if !rule.Match(room, event) { - continue - } - return rule.Actions - } - return nil -} - -type PushRuleMap struct { - Map map[string]*PushRule - Type PushRuleType -} - -func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap { - data := PushRuleMap{ - Map: make(map[string]*PushRule), - Type: typ, - } - for _, rule := range rules { - rule.Type = typ - data.Map[rule.RuleID] = rule - } - return data -} - -func (ruleMap PushRuleMap) GetActions(room Room, event *mautrix.Event) PushActionArray { - var rule *PushRule - var found bool - switch ruleMap.Type { - case RoomRule: - rule, found = ruleMap.Map[event.RoomID] - case SenderRule: - rule, found = ruleMap.Map[event.Sender] - } - if found && rule.Match(room, event) { - return rule.Actions - } - return nil -} - -func (ruleMap PushRuleMap) Unmap() PushRuleArray { - array := make(PushRuleArray, len(ruleMap.Map)) - index := 0 - for _, rule := range ruleMap.Map { - array[index] = rule - index++ - } - return array -} - -type PushRuleType string - -const ( - OverrideRule PushRuleType = "override" - ContentRule PushRuleType = "content" - RoomRule PushRuleType = "room" - SenderRule PushRuleType = "sender" - UnderrideRule PushRuleType = "underride" -) - -type PushRule struct { - // The type of this rule. - Type PushRuleType `json:"-"` - // The ID of this rule. - // For room-specific rules and user-specific rules, this is the room or user ID (respectively) - // For other types of rules, this doesn't affect anything. - RuleID string `json:"rule_id"` - // The actions this rule should trigger when matched. - Actions PushActionArray `json:"actions"` - // Whether this is a default rule, or has been set explicitly. - Default bool `json:"default"` - // Whether or not this push rule is enabled. - Enabled bool `json:"enabled"` - // The conditions to match in order to trigger this rule. - // Only applicable to generic underride/override rules. - Conditions []*PushCondition `json:"conditions,omitempty"` - // Pattern for content-specific push rules - Pattern string `json:"pattern,omitempty"` -} - -func (rule *PushRule) Match(room Room, event *mautrix.Event) bool { - if !rule.Enabled { - return false - } - switch rule.Type { - case OverrideRule, UnderrideRule: - return rule.matchConditions(room, event) - case ContentRule: - return rule.matchPattern(room, event) - case RoomRule: - return rule.RuleID == event.RoomID - case SenderRule: - return rule.RuleID == event.Sender - default: - return false - } -} - -func (rule *PushRule) matchConditions(room Room, event *mautrix.Event) bool { - for _, cond := range rule.Conditions { - if !cond.Match(room, event) { - return false - } - } - return true -} - -func (rule *PushRule) matchPattern(room Room, event *mautrix.Event) bool { - pattern, err := glob.Compile(rule.Pattern) - if err != nil { - return false - } - return pattern.MatchString(event.Content.Body) -} diff --git a/matrix/pushrules/rule_array_test.go b/matrix/pushrules/rule_array_test.go deleted file mode 100644 index 8bfc5e9..0000000 --- a/matrix/pushrules/rule_array_test.go +++ /dev/null @@ -1,294 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/pushrules" - "maunium.net/go/mautrix" - "testing" -) - -func TestPushRuleArray_GetActions_FirstMatchReturns(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "no match") - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!fakeroom:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@tulir:maunium.net", - Actions: actions3, - } - - rules := pushrules.PushRuleArray{rule1, rule2, rule3} - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Equal(t, rules.GetActions(blankTestRoom, event), actions2) -} - -func TestPushRuleArray_GetActions_NoMatchesIsNil(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "no match") - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!realroom:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@otheruser:maunium.net", - Actions: actions3, - } - - rules := pushrules.PushRuleArray{rule1, rule2, rule3} - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Nil(t, rules.GetActions(blankTestRoom, event)) -} - -func TestPushRuleMap_GetActions_RoomRuleExists(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!realroom:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!thirdroom:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!fakeroom:maunium.net", - Actions: actions3, - } - - rules := pushrules.PushRuleMap{ - Map: map[string]*pushrules.PushRule{ - rule1.RuleID: rule1, - rule2.RuleID: rule2, - rule3.RuleID: rule3, - }, - Type: pushrules.RoomRule, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Equal(t, rules.GetActions(blankTestRoom, event), actions3) -} - -func TestPushRuleMap_GetActions_RoomRuleDoesntExist(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!realroom:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!thirdroom:maunium.net", - Actions: actions2, - } - - rules := pushrules.PushRuleMap{ - Map: map[string]*pushrules.PushRule{ - rule1.RuleID: rule1, - rule2.RuleID: rule2, - }, - Type: pushrules.RoomRule, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Nil(t, rules.GetActions(blankTestRoom, event)) -} - -func TestPushRuleMap_GetActions_SenderRuleExists(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@tulir:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@someone:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@otheruser:matrix.org", - Actions: actions3, - } - - rules := pushrules.PushRuleMap{ - Map: map[string]*pushrules.PushRule{ - rule1.RuleID: rule1, - rule2.RuleID: rule2, - rule3.RuleID: rule3, - }, - Type: pushrules.SenderRule, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Equal(t, rules.GetActions(blankTestRoom, event), actions1) -} - -func TestPushRuleArray_SetTypeAndMap(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Enabled: true, - RuleID: "@tulir:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Enabled: true, - RuleID: "@someone:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Enabled: true, - RuleID: "@otheruser:matrix.org", - Actions: actions3, - } - - ruleArray := pushrules.PushRuleArray{rule1, rule2, rule3} - ruleMap := ruleArray.SetTypeAndMap(pushrules.SenderRule) - assert.Equal(t, pushrules.SenderRule, ruleMap.Type) - for _, rule := range ruleArray { - assert.Equal(t, rule, ruleMap.Map[rule.RuleID]) - } - newRuleArray := ruleMap.Unmap() - for _, rule := range ruleArray { - assert.Contains(t, newRuleArray, rule) - } -} diff --git a/matrix/pushrules/rule_test.go b/matrix/pushrules/rule_test.go deleted file mode 100644 index 56d48fd..0000000 --- a/matrix/pushrules/rule_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/pushrules" - "maunium.net/go/mautrix" - "testing" -) - -func TestPushRule_Match_Conditions(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "*pushrules") - rule := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.emote", - "body": "is testing pushrules", - }, - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Conditions_Disabled(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "*pushrules") - rule := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: false, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.emote", - "body": "is testing pushrules", - }, - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Conditions_FailIfOneFails(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "*pushrules") - rule := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.text", - "body": "I'm testing pushrules", - }, - MsgType: mautrix.MsgText, - Body: "I'm testing pushrules", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "is testing*", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content_Fail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "is testing*", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is not testing pushrules", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content_ImplicitGlob(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "testing", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is not testing pushrules", - }) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content_IllegalGlob(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "this is not a valid glo[b", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "this is not a valid glob", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Room(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!fakeroom:maunium.net", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Room_Fail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!otherroom:maunium.net", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Sender(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@tulir:maunium.net", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Sender_Fail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "@someone:matrix.org", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_UnknownTypeAlwaysFail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.PushRuleType("foobar"), - Enabled: true, - RuleID: "@someone:matrix.org", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.False(t, rule.Match(blankTestRoom, event)) -} diff --git a/matrix/pushrules/ruleset.go b/matrix/pushrules/ruleset.go deleted file mode 100644 index 7ad931a..0000000 --- a/matrix/pushrules/ruleset.go +++ /dev/null @@ -1,98 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import ( - "encoding/json" - - "maunium.net/go/mautrix" -) - -type PushRuleset struct { - Override PushRuleArray - Content PushRuleArray - Room PushRuleMap - Sender PushRuleMap - Underride PushRuleArray -} - -type rawPushRuleset struct { - Override PushRuleArray `json:"override"` - Content PushRuleArray `json:"content"` - Room PushRuleArray `json:"room"` - Sender PushRuleArray `json:"sender"` - Underride PushRuleArray `json:"underride"` -} - -// UnmarshalJSON parses JSON into this PushRuleset. -// -// For override, sender and underride push rule arrays, the type is added -// to each PushRule and the array is used as-is. -// -// For room and sender push rule arrays, the type is added to each PushRule -// and the array is converted to a map with the rule ID as the key and the -// PushRule as the value. -func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) { - data := rawPushRuleset{} - err = json.Unmarshal(raw, &data) - if err != nil { - return - } - - rs.Override = data.Override.SetType(OverrideRule) - rs.Content = data.Content.SetType(ContentRule) - rs.Room = data.Room.SetTypeAndMap(RoomRule) - rs.Sender = data.Sender.SetTypeAndMap(SenderRule) - rs.Underride = data.Underride.SetType(UnderrideRule) - return -} - -// MarshalJSON is the reverse of UnmarshalJSON() -func (rs *PushRuleset) MarshalJSON() ([]byte, error) { - data := rawPushRuleset{ - Override: rs.Override, - Content: rs.Content, - Room: rs.Room.Unmap(), - Sender: rs.Sender.Unmap(), - Underride: rs.Underride, - } - return json.Marshal(&data) -} - -// DefaultPushActions is the value returned if none of the rule -// collections in a Ruleset match the event given to GetActions() -var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}} - -// GetActions matches the given event against all of the push rule -// collections in this push ruleset in the order of priority as -// specified in spec section 11.12.1.4. -func (rs *PushRuleset) GetActions(room Room, event *mautrix.Event) (match PushActionArray) { - // Add push rule collections to array in priority order - arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} - // Loop until one of the push rule collections matches the room/event combo. - for _, pra := range arrays { - if pra == nil { - continue - } - if match = pra.GetActions(room, event); match != nil { - // Match found, return it. - return - } - } - // No match found, return default actions. - return DefaultPushActions -} diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index fd7d53b..0c28a9d 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -27,6 +27,8 @@ import ( sync "github.com/sasha-s/go-deadlock" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/debug" ) @@ -54,25 +56,27 @@ type RoomTag struct { } type UnreadMessage struct { - EventID string + EventID id.EventID Counted bool Highlight bool } type Member struct { - mautrix.Member + event.Member // The user who sent the membership event - Sender string `json:"-"` + Sender id.UserID `json:"-"` } // Room represents a single Matrix room. type Room struct { // The room ID. - ID string + ID id.RoomID // Whether or not the user has left the room. HasLeft bool + // Whether or not the room is encrypted. + Encrypted bool // The first batch of events that has been fetched for this room. // Used for fetching additional history. @@ -80,14 +84,14 @@ type Room struct { // The last_batch field from the most recent sync. Used for fetching member lists. LastPrevBatch string // The MXID of the user whose session this room was created for. - SessionUserID string + SessionUserID id.UserID SessionMember *Member // The number of unread messages that were notified about. UnreadMessages []UnreadMessage unreadCountCache *int highlightCache *bool - lastMarkedRead string + lastMarkedRead id.EventID // Whether or not this room is marked as a direct chat. IsDirect bool @@ -101,10 +105,10 @@ type Room struct { // Whether or not the members for this room have been fetched from the server. MembersFetched bool // Room state cache. - state map[mautrix.EventType]map[string]*mautrix.Event + state map[event.Type]map[string]*event.Event // MXID -> Member cache calculated from membership events. - memberCache map[string]*Member - exMemberCache map[string]*Member + memberCache map[id.UserID]*Member + exMemberCache map[id.UserID]*Member // The first two non-SessionUserID members in the room. Calculated at // the same time as memberCache. firstMemberCache *Member @@ -117,11 +121,11 @@ type Room struct { // The topic of the room. Directly fetched from the m.room.topic state event. topicCache string // The canonical alias of the room. Directly fetched from the m.room.canonical_alias state event. - CanonicalAliasCache string + CanonicalAliasCache id.RoomAlias // Whether or not the room has been tombstoned. replacedCache bool // The room ID that replaced this room. - replacedByCache *string + replacedByCache *id.RoomID // Path for state store file. path string @@ -174,7 +178,7 @@ func (room *Room) load() { return } debug.Print("Loading state for room", room.ID, "from disk") - room.state = make(map[mautrix.EventType]map[string]*mautrix.Event) + room.state = make(map[event.Type]map[string]*event.Event) file, err := os.OpenFile(room.path, os.O_RDONLY, 0600) if err != nil { if !os.IsNotExist(err) { @@ -265,7 +269,7 @@ func (room *Room) Save() { } // MarkRead clears the new message statuses on this room. -func (room *Room) MarkRead(eventID string) bool { +func (room *Room) MarkRead(eventID id.EventID) bool { room.lock.Lock() defer room.lock.Unlock() if room.lastMarkedRead == eventID { @@ -319,7 +323,7 @@ func (room *Room) HasNewMessages() bool { return len(room.UnreadMessages) > 0 } -func (room *Room) AddUnread(eventID string, counted, highlight bool) { +func (room *Room) AddUnread(eventID id.EventID, counted, highlight bool) { room.lock.Lock() defer room.lock.Unlock() room.UnreadMessages = append(room.UnreadMessages, UnreadMessage{ @@ -341,18 +345,25 @@ func (room *Room) AddUnread(eventID string, counted, highlight bool) { } } +var ( + tagDirect = RoomTag{"net.maunium.gomuks.fake.direct", "0.5"} + tagInvite = RoomTag{"net.maunium.gomuks.fake.invite", "0.5"} + tagDefault = RoomTag{"", "0.5"} + tagLeave = RoomTag{"net.maunium.gomuks.fake.leave", "0.5"} +) + func (room *Room) Tags() []RoomTag { room.lock.RLock() defer room.lock.RUnlock() if len(room.RawTags) == 0 { if room.IsDirect { - return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}} - } else if room.SessionMember != nil && room.SessionMember.Membership == mautrix.MembershipInvite { - return []RoomTag{{"net.maunium.gomuks.fake.invite", "0.5"}} - } else if room.SessionMember != nil && room.SessionMember.Membership != mautrix.MembershipJoin { - return []RoomTag{{"net.maunium.gomuks.fake.leave", "0.5"}} + return []RoomTag{tagDirect} + } else if room.SessionMember != nil && room.SessionMember.Membership == event.MembershipInvite { + return []RoomTag{tagInvite} + } else if room.SessionMember != nil && room.SessionMember.Membership != event.MembershipJoin { + return []RoomTag{tagLeave} } - return []RoomTag{{"", "0.5"}} + return []RoomTag{tagDefault} } return room.RawTags } @@ -374,46 +385,46 @@ func (room *Room) UpdateSummary(summary mautrix.LazyLoadSummary) { // UpdateState updates the room's current state with the given Event. This will clobber events based // on the type/state_key combination. -func (room *Room) UpdateState(event *mautrix.Event) { - if event.StateKey == nil { +func (room *Room) UpdateState(evt *event.Event) { + if evt.StateKey == nil { panic("Tried to UpdateState() event with no state key.") } room.Load() room.lock.Lock() defer room.lock.Unlock() room.changed = true - _, exists := room.state[event.Type] + _, exists := room.state[evt.Type] if !exists { - room.state[event.Type] = make(map[string]*mautrix.Event) + room.state[evt.Type] = make(map[string]*event.Event) } - switch event.Type { - case mautrix.StateRoomName: - room.NameCache = event.Content.Name + switch evt.Type { + case event.StateRoomName: + room.NameCache = evt.Content.Name room.nameCacheSource = ExplicitRoomName - case mautrix.StateCanonicalAlias: + case event.StateCanonicalAlias: if room.nameCacheSource <= CanonicalAliasRoomName { - room.NameCache = event.Content.Alias + room.NameCache = string(evt.Content.Alias) room.nameCacheSource = CanonicalAliasRoomName } - room.CanonicalAliasCache = event.Content.Alias - case mautrix.StateMember: + room.CanonicalAliasCache = evt.Content.Alias + case event.StateMember: if room.nameCacheSource <= MemberRoomName { room.NameCache = "" } - room.updateMemberState(event) - case mautrix.StateTopic: - room.topicCache = event.Content.Topic + room.updateMemberState(evt) + case event.StateTopic: + room.topicCache = evt.Content.Topic } - if event.Type != mautrix.StateMember { - debug.Printf("Updating state %s#%s for %s", event.Type.String(), event.GetStateKey(), room.ID) + if evt.Type != event.StateMember { + debug.Printf("Updating state %s#%s for %s", evt.Type.String(), evt.GetStateKey(), room.ID) } - room.state[event.Type][*event.StateKey] = event + room.state[evt.Type][*evt.StateKey] = evt } -func (room *Room) updateMemberState(event *mautrix.Event) { - userID := event.GetStateKey() +func (room *Room) updateMemberState(event *event.Event) { + userID := id.UserID(event.GetStateKey()) if userID == room.SessionUserID { debug.Print("Updating session user state:", string(event.Content.VeryRaw)) room.SessionMember = room.eventToMember(userID, event.Sender, &event.Content) @@ -442,7 +453,7 @@ func (room *Room) updateMemberState(event *mautrix.Event) { } // GetStateEvent returns the state event for the given type/state_key combo, or nil. -func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event { +func (room *Room) GetStateEvent(eventType event.Type, stateKey string) *event.Event { room.Load() room.lock.RLock() defer room.lock.RUnlock() @@ -452,7 +463,7 @@ func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *m } // getStateEvents returns the state events for the given type. -func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event { +func (room *Room) getStateEvents(eventType event.Type) map[string]*event.Event { stateEventMap, _ := room.state[eventType] return stateEventMap } @@ -460,7 +471,7 @@ func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautri // GetTopic returns the topic of the room. func (room *Room) GetTopic() string { if len(room.topicCache) == 0 { - topicEvt := room.GetStateEvent(mautrix.StateTopic, "") + topicEvt := room.GetStateEvent(event.StateTopic, "") if topicEvt != nil { room.topicCache = topicEvt.Content.Topic } @@ -468,9 +479,9 @@ func (room *Room) GetTopic() string { return room.topicCache } -func (room *Room) GetCanonicalAlias() string { +func (room *Room) GetCanonicalAlias() id.RoomAlias { if len(room.CanonicalAliasCache) == 0 { - canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "") + canonicalAliasEvt := room.GetStateEvent(event.StateCanonicalAlias, "") if canonicalAliasEvt != nil { room.CanonicalAliasCache = canonicalAliasEvt.Content.Alias } else { @@ -485,7 +496,7 @@ func (room *Room) GetCanonicalAlias() string { // updateNameFromNameEvent updates the room display name to be the name set in the name event. func (room *Room) updateNameFromNameEvent() { - nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") + nameEvt := room.GetStateEvent(event.StateRoomName, "") if nameEvt != nil { room.NameCache = nameEvt.Content.Name } @@ -528,7 +539,7 @@ func (room *Room) updateNameCache() { room.nameCacheSource = ExplicitRoomName } if len(room.NameCache) == 0 { - room.NameCache = room.GetCanonicalAlias() + room.NameCache = string(room.GetCanonicalAlias()) room.nameCacheSource = CanonicalAliasRoomName } if len(room.NameCache) == 0 { @@ -548,8 +559,8 @@ func (room *Room) GetTitle() string { func (room *Room) IsReplaced() bool { if room.replacedByCache == nil { - evt := room.GetStateEvent(mautrix.StateTombstone, "") - var replacement string + evt := room.GetStateEvent(event.StateTombstone, "") + var replacement id.RoomID if evt != nil { replacement = evt.Content.ReplacementRoom } @@ -559,18 +570,18 @@ func (room *Room) IsReplaced() bool { return room.replacedCache } -func (room *Room) ReplacedBy() string { +func (room *Room) ReplacedBy() id.RoomID { if room.replacedByCache == nil { room.IsReplaced() } return *room.replacedByCache } -func (room *Room) eventToMember(userID string, sender string, content *mautrix.Content) *Member { +func (room *Room) eventToMember(userID, sender id.UserID, content *event.Content) *Member { member := content.Member member.Membership = content.Membership if len(member.Displayname) == 0 { - member.Displayname = userID + member.Displayname = string(userID) } return &Member{ Member: member, @@ -578,7 +589,7 @@ func (room *Room) eventToMember(userID string, sender string, content *mautrix.C } } -func (room *Room) updateNthMemberCache(userID string, member *Member) { +func (room *Room) updateNthMemberCache(userID id.UserID, member *Member) { if userID != room.SessionUserID { if room.firstMemberCache == nil { room.firstMemberCache = member @@ -589,19 +600,20 @@ func (room *Room) updateNthMemberCache(userID string, member *Member) { } // createMemberCache caches all member events into a easily processable MXID -> *Member map. -func (room *Room) createMemberCache() map[string]*Member { +func (room *Room) createMemberCache() map[id.UserID]*Member { if len(room.memberCache) > 0 { return room.memberCache } - cache := make(map[string]*Member) - exCache := make(map[string]*Member) + cache := make(map[id.UserID]*Member) + exCache := make(map[id.UserID]*Member) room.lock.RLock() - events := room.getStateEvents(mautrix.StateMember) + memberEvents := room.getStateEvents(event.StateMember) room.firstMemberCache = nil room.secondMemberCache = nil - if events != nil { - for userID, event := range events { - member := room.eventToMember(userID, event.Sender, &event.Content) + if memberEvents != nil { + for userIDStr, evt := range memberEvents { + userID := id.UserID(userIDStr) + member := room.eventToMember(userID, evt.Sender, &evt.Content) if member.Membership.IsInviteOrJoin() { cache[userID] = member room.updateNthMemberCache(userID, member) @@ -631,7 +643,7 @@ func (room *Room) createMemberCache() map[string]*Member { // // The members are returned from the cache. // If the cache is empty, it is updated first. -func (room *Room) GetMembers() map[string]*Member { +func (room *Room) GetMembers() map[id.UserID]*Member { room.Load() room.createMemberCache() return room.memberCache @@ -639,7 +651,7 @@ func (room *Room) GetMembers() map[string]*Member { // GetMember returns the member with the given MXID. // If the member doesn't exist, nil is returned. -func (room *Room) GetMember(userID string) *Member { +func (room *Room) GetMember(userID id.UserID) *Member { if userID == room.SessionUserID && room.SessionMember != nil { return room.SessionMember } @@ -660,16 +672,27 @@ func (room *Room) GetMember(userID string) *Member { return nil } +func (room *Room) GetMemberCount() int { + if room.memberCache == nil && room.Summary.JoinedMemberCount != nil { + return *room.Summary.JoinedMemberCount + } + return len(room.GetMembers()) +} + // GetSessionOwner returns the ID of the user whose session this room was created for. -func (room *Room) GetSessionOwner() string { - return room.SessionUserID +func (room *Room) GetOwnDisplayname() string { + member := room.GetMember(room.SessionUserID) + if member != nil { + return member.Displayname + } + return "" } // NewRoom creates a new Room with the given ID -func NewRoom(roomID string, cache *RoomCache) *Room { +func NewRoom(roomID id.RoomID, cache *RoomCache) *Room { return &Room{ ID: roomID, - state: make(map[mautrix.EventType]map[string]*mautrix.Event), + state: make(map[event.Type]map[string]*event.Event), path: cache.roomPath(roomID), cache: cache, diff --git a/matrix/rooms/room_test.go b/matrix/rooms/room_test.go deleted file mode 100644 index a1fc4a4..0000000 --- a/matrix/rooms/room_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package rooms_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" -) - -func TestNewRoom_DefaultValues(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - assert.Equal(t, "!test:maunium.net", room.ID) - assert.Equal(t, "@tulir:maunium.net", room.SessionUserID) - assert.Empty(t, room.GetMembers()) - assert.Equal(t, "Empty room", room.GetTitle()) - assert.Empty(t, room.GetAliases()) - assert.Empty(t, room.GetCanonicalAlias()) - assert.Empty(t, room.GetTopic()) - assert.Nil(t, room.GetMember(room.GetSessionOwner())) -} - -func TestRoom_GetCanonicalAlias(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateCanonicalAlias, - Content: mautrix.Content{ - Alias: "#foo:maunium.net", - }, - }) - assert.Equal(t, "#foo:maunium.net", room.GetCanonicalAlias()) -} - -func TestRoom_GetTopic(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateTopic, - Content: mautrix.Content{ - Topic: "test topic", - }, - }) - assert.Equal(t, "test topic", room.GetTopic()) -} - -func TestRoom_Tags_Empty(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - assert.Empty(t, room.RawTags) - tags := room.Tags() - assert.Len(t, tags, 1) - assert.Equal(t, "", tags[0].Tag) - assert.Equal(t, "0.5", tags[0].Order) -} - -func TestRoom_Tags_NotEmpty(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.RawTags = []rooms.RoomTag{{Tag: "foo", Order: "1"}, {Tag: "bar", Order: "1"}} - tags := room.Tags() - assert.Equal(t, room.RawTags, tags) -} - -func TestRoom_GetAliases(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addAliases(room) - - aliases := room.GetAliases() - assert.Contains(t, aliases, "#bar:maunium.net") - assert.Contains(t, aliases, "#test:maunium.net") - assert.Contains(t, aliases, "#foo:matrix.org") - assert.Contains(t, aliases, "#test:matrix.org") -} - -func addName(room *rooms.Room) { - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateRoomName, - Content: mautrix.Content{ - Name: "Test room", - }, - }) -} - -func addCanonicalAlias(room *rooms.Room) { - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateCanonicalAlias, - Content: mautrix.Content{ - Alias: "#foo:maunium.net", - }, - }) -} - -func addAliases(room *rooms.Room) { - server1 := "maunium.net" - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateAliases, - StateKey: &server1, - Content: mautrix.Content{ - Aliases: []string{"#bar:maunium.net", "#test:maunium.net", "#foo:maunium.net"}, - }, - }) - - server2 := "matrix.org" - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateAliases, - StateKey: &server2, - Content: mautrix.Content{ - Aliases: []string{"#foo:matrix.org", "#test:matrix.org"}, - }, - }) -} - -func addMembers(room *rooms.Room, count int) { - user1 := "@tulir:maunium.net" - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateMember, - StateKey: &user1, - Content: mautrix.Content{ - Member: mautrix.Member{ - Displayname: "tulir", - Membership: mautrix.MembershipJoin, - }, - }, - }) - - for i := 1; i < count; i++ { - userN := fmt.Sprintf("@user_%d:matrix.org", i+1) - content := mautrix.Content{ - Member: mautrix.Member{ - Membership: mautrix.MembershipJoin, - }, - } - if i%2 == 1 { - content.Displayname = fmt.Sprintf("User #%d", i+1) - } - if i%5 == 0 { - content.Membership = mautrix.MembershipInvite - } - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateMember, - StateKey: &userN, - Content: content, - }) - } -} - -func TestRoom_GetMembers(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 6) - - members := room.GetMembers() - assert.Len(t, members, 6) -} - -func TestRoom_GetMember(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 6) - - assert.NotNil(t, room.GetMember("@user_2:matrix.org")) - assert.NotNil(t, room.GetMember("@tulir:maunium.net")) - assert.Equal(t, "@tulir:maunium.net", room.GetSessionOwner()) -} - -func TestRoom_GetTitle_ExplicitName(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 4) - addName(room) - addCanonicalAlias(room) - addAliases(room) - assert.Equal(t, "Test room", room.GetTitle()) -} - -func TestRoom_GetTitle_CanonicalAlias(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 4) - addCanonicalAlias(room) - addAliases(room) - assert.Equal(t, "#foo:maunium.net", room.GetTitle()) -} - -func TestRoom_GetTitle_FirstAlias(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 2) - addAliases(room) - assert.Equal(t, "#bar:maunium.net", room.GetTitle()) -} - -func TestRoom_GetTitle_Members_Empty(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 1) - assert.Equal(t, "Empty room", room.GetTitle()) -} - -func TestRoom_GetTitle_Members_OneToOne(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 2) - assert.Equal(t, "User #2", room.GetTitle()) -} - -func TestRoom_GetTitle_Members_GroupChat(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 76) - assert.Contains(t, room.GetTitle(), " and 74 others") -} - -func TestRoom_MarkRead(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - - room.AddUnread("foo", true, false) - assert.Equal(t, 1, room.UnreadCount()) - assert.False(t, room.Highlighted()) - - room.AddUnread("bar", true, false) - assert.Equal(t, 2, room.UnreadCount()) - assert.False(t, room.Highlighted()) - - room.AddUnread("asd", false, true) - assert.Equal(t, 2, room.UnreadCount()) - assert.True(t, room.Highlighted()) - - room.MarkRead("asd") - assert.Empty(t, room.UnreadMessages) -} diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go index 6fc400c..d442734 100644 --- a/matrix/rooms/roomcache.go +++ b/matrix/rooms/roomcache.go @@ -27,6 +27,7 @@ import ( sync "github.com/sasha-s/go-deadlock" "maunium.net/go/gomuks/debug" + "maunium.net/go/mautrix/id" ) // RoomCache contains room state info in a hashmap and linked list. @@ -37,15 +38,15 @@ type RoomCache struct { directory string maxSize int maxAge int64 - getOwner func() string + getOwner func() id.UserID - Map map[string]*Room + Map map[id.RoomID]*Room head *Room tail *Room size int } -func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() string) *RoomCache { +func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() id.UserID) *RoomCache { return &RoomCache{ listPath: listPath, directory: directory, @@ -53,7 +54,7 @@ func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwne maxAge: maxAge, getOwner: getOwner, - Map: make(map[string]*Room), + Map: make(map[id.RoomID]*Room), } } @@ -88,7 +89,7 @@ func (cache *RoomCache) LoadList() error { } // Read list - cache.Map = make(map[string]*Room, size) + cache.Map = make(map[id.RoomID]*Room, size) for i := 0; i < size; i++ { room := &Room{} err = dec.Decode(room) @@ -147,7 +148,7 @@ func (cache *RoomCache) SaveList() error { return nil } -func (cache *RoomCache) Touch(roomID string) { +func (cache *RoomCache) Touch(roomID id.RoomID) { cache.Lock() node, ok := cache.Map[roomID] if !ok || node == nil { @@ -174,14 +175,14 @@ func (cache *RoomCache) touch(node *Room) { node.touch = time.Now().Unix() } -func (cache *RoomCache) Get(roomID string) *Room { +func (cache *RoomCache) Get(roomID id.RoomID) *Room { cache.Lock() node := cache.get(roomID) cache.Unlock() return node } -func (cache *RoomCache) GetOrCreate(roomID string) *Room { +func (cache *RoomCache) GetOrCreate(roomID id.RoomID) *Room { cache.Lock() node := cache.get(roomID) if node == nil { @@ -192,7 +193,7 @@ func (cache *RoomCache) GetOrCreate(roomID string) *Room { return node } -func (cache *RoomCache) get(roomID string) *Room { +func (cache *RoomCache) get(roomID id.RoomID) *Room { node, ok := cache.Map[roomID] if ok && node != nil { return node @@ -215,11 +216,11 @@ func (cache *RoomCache) Put(room *Room) { node.Save() } -func (cache *RoomCache) roomPath(roomID string) string { - return filepath.Join(cache.directory, roomID+".gob.gz") +func (cache *RoomCache) roomPath(roomID id.RoomID) string { + return filepath.Join(cache.directory, string(roomID)+".gob.gz") } -func (cache *RoomCache) Load(roomID string) *Room { +func (cache *RoomCache) Load(roomID id.RoomID) *Room { cache.Lock() defer cache.Unlock() node, ok := cache.Map[roomID] @@ -312,7 +313,7 @@ func (cache *RoomCache) Unload(node *Room) { } } -func (cache *RoomCache) newRoom(roomID string) *Room { +func (cache *RoomCache) newRoom(roomID id.RoomID) *Room { node := NewRoom(roomID, cache) cache.Map[node.ID] = node return node diff --git a/matrix/sync.go b/matrix/sync.go index 8ec22b5..53c1798 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -24,14 +24,16 @@ import ( "time" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/rooms" ) type SyncerSession interface { - GetRoom(id string) *rooms.Room - GetUserID() string + GetRoom(id id.RoomID) *rooms.Room + GetUserID() id.UserID } type EventSource int @@ -45,6 +47,7 @@ const ( EventSourceTimeline EventSourceState EventSourceEphemeral + EventSourceToDevice ) func (es EventSource) String() string { @@ -83,14 +86,14 @@ func (es EventSource) String() string { return fmt.Sprintf("unknown (%d)", es) } -type EventHandler func(source EventSource, event *mautrix.Event) +type EventHandler func(source EventSource, event *event.Event) // GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively // replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer // pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information. type GomuksSyncer struct { Session SyncerSession - listeners map[mautrix.EventType][]EventHandler // event type to listeners array + listeners map[event.Type][]EventHandler // event type to listeners array FirstSyncDone bool InitDoneCallback func() } @@ -99,7 +102,7 @@ type GomuksSyncer struct { func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { return &GomuksSyncer{ Session: session, - listeners: make(map[mautrix.EventType][]EventHandler), + listeners: make(map[event.Type][]EventHandler), FirstSyncDone: false, } } @@ -152,33 +155,44 @@ func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err } func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []json.RawMessage, source EventSource) { - for _, event := range events { - if source == EventSourcePresence { - debug.Print(string(event)) - } - s.processSyncEvent(room, event, source) + for _, evt := range events { + s.processSyncEvent(room, evt, source) } } func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, eventJSON json.RawMessage, source EventSource) { - event := &mautrix.Event{} - err := json.Unmarshal(eventJSON, event) + evt := &event.Event{} + err := json.Unmarshal(eventJSON, evt) if err != nil { debug.Print("Failed to unmarshal event: %v\n%s", err, string(eventJSON)) return } + // Ensure the type class is correct. It's safe to mutate since it's not a pointer. + // Listeners are keyed by type structs, which means only the correct class will pass. + switch { + case evt.StateKey != nil: + evt.Type.Class = event.StateEventType + case source == EventSourcePresence, source & EventSourceEphemeral != 0: + evt.Type.Class = event.EphemeralEventType + case source & EventSourceAccountData != 0: + evt.Type.Class = event.AccountDataEventType + case source == EventSourceToDevice: + evt.Type.Class = event.ToDeviceEventType + default: + evt.Type.Class = event.MessageEventType + } if room != nil { - event.RoomID = room.ID - if source&EventSourceState != 0 || (source&EventSourceTimeline != 0 && event.Type.IsState() && event.StateKey != nil) { - room.UpdateState(event) + evt.RoomID = room.ID + if evt.Type.IsState() { + room.UpdateState(evt) } } - s.notifyListeners(source, event) + s.notifyListeners(source, evt) } // OnEventType allows callers to be notified when there are new events for the given event type. // There are no duplicate checks. -func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHandler) { +func (s *GomuksSyncer) OnEventType(eventType event.Type, callback EventHandler) { _, exists := s.listeners[eventType] if !exists { s.listeners[eventType] = []EventHandler{} @@ -186,21 +200,13 @@ func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHa s.listeners[eventType] = append(s.listeners[eventType], callback) } -func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) { - if (event.Type.IsState() && source&EventSourceState == 0 && event.StateKey == nil) || - (event.Type.IsAccountData() && source&EventSourceAccountData == 0) || - (event.Type.IsEphemeral() && event.Type != mautrix.EphemeralEventPresence && source&EventSourceEphemeral == 0) || - (event.Type == mautrix.EphemeralEventPresence && source&EventSourcePresence == 0) { - evtJson, _ := json.Marshal(event) - debug.Printf("Event of type %s received from mismatching source %s: %s", event.Type.String(), source.String(), string(evtJson)) - return - } - listeners, exists := s.listeners[event.Type] +func (s *GomuksSyncer) notifyListeners(source EventSource, evt *event.Event) { + listeners, exists := s.listeners[evt.Type] if !exists { return } for _, fn := range listeners { - fn(source, event) + fn(source, evt) } } @@ -211,53 +217,51 @@ func (s *GomuksSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Dura } // GetFilterJSON returns a filter with a timeline limit of 50. -func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { +func (s *GomuksSyncer) GetFilterJSON(_ id.UserID) json.RawMessage { filter := &mautrix.Filter{ Room: mautrix.RoomFilter{ IncludeLeave: false, State: mautrix.FilterPart{ LazyLoadMembers: true, - Types: []string{ - "m.room.member", - "m.room.name", - "m.room.topic", - "m.room.canonical_alias", - "m.room.aliases", - "m.room.power_levels", - "m.room.tombstone", + Types: []event.Type{ + event.StateMember, + event.StateRoomName, + event.StateTopic, + event.StateCanonicalAlias, + event.StatePowerLevels, + event.StateTombstone, }, }, Timeline: mautrix.FilterPart{ LazyLoadMembers: true, - Types: []string{ - "m.room.message", - "m.room.redaction", - "m.room.encrypted", - "m.sticker", - "m.reaction", + Types: []event.Type{ + event.EventMessage, + event.EventRedaction, + event.EventEncrypted, + event.EventSticker, + event.EventReaction, - "m.room.member", - "m.room.name", - "m.room.topic", - "m.room.canonical_alias", - "m.room.aliases", - "m.room.power_levels", - "m.room.tombstone", + event.StateMember, + event.StateRoomName, + event.StateTopic, + event.StateCanonicalAlias, + event.StatePowerLevels, + event.StateTombstone, }, -// Limit: 50, + Limit: 50, }, Ephemeral: mautrix.FilterPart{ - Types: []string{"m.typing", "m.receipt"}, + Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}, }, AccountData: mautrix.FilterPart{ - Types: []string{"m.tag"}, + Types: []event.Type{event.AccountDataRoomTags}, }, }, AccountData: mautrix.FilterPart{ - Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"}, + Types: []event.Type{event.AccountDataPushRules, event.AccountDataDirectChats, AccountDataGomuksPreferences}, }, Presence: mautrix.FilterPart{ - NotTypes: []string{"*"}, + NotTypes: []event.Type{event.NewEventType("*")}, }, } rawFilter, _ := json.Marshal(&filter) diff --git a/matrix/sync_test.go b/matrix/sync_test.go deleted file mode 100644 index 4b85a75..0000000 --- a/matrix/sync_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package matrix_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" -) - -func TestGomuksSyncer_ProcessResponse_Initial(t *testing.T) { - syncer := matrix.NewGomuksSyncer(&mockSyncerSession{}) - var initDoneCalled = false - syncer.InitDoneCallback = func() { - initDoneCalled = true - } - - syncer.ProcessResponse(newRespSync(), "") - assert.True(t, syncer.FirstSyncDone) - assert.True(t, initDoneCalled) -} - -func TestGomuksSyncer_ProcessResponse(t *testing.T) { - mss := &mockSyncerSession{ - userID: "@tulir:maunium.net", - rooms: map[string]*rooms.Room{ - "!foo:maunium.net": { - Room: mautrix.NewRoom("!foo:maunium.net"), - }, - "!bar:maunium.net": { - Room: mautrix.NewRoom("!bar:maunium.net"), - }, - "!test:maunium.net": { - Room: mautrix.NewRoom("!test:maunium.net"), - }, - }, - } - ml := &mockListener{} - syncer := matrix.NewGomuksSyncer(mss) - syncer.OnEventType(mautrix.EventMessage, ml.receive) - syncer.OnEventType(mautrix.StateMember, ml.receive) - syncer.GetFilterJSON("@tulir:maunium.net") - - joinEvt := &mautrix.Event{ - ID: "!join:maunium.net", - Type: mautrix.StateMember, - Sender: "@tulir:maunium.net", - StateKey: ptr("Ì£@tulir:maunium.net"), - Content: mautrix.Content{ - Membership: mautrix.MembershipJoin, - }, - } - messageEvt := &mautrix.Event{ - ID: "!msg:maunium.net", - Type: mautrix.EventMessage, - Content: mautrix.Content{ - Body: "foo", - MsgType: mautrix.MsgText, - }, - } - unhandledEvt := &mautrix.Event{ - ID: "!unhandled:maunium.net", - Type: mautrix.EventType{Type: "m.room.unhandled_event"}, - } - inviteEvt := &mautrix.Event{ - ID: "!invite:matrix.org", - Type: mautrix.StateMember, - Sender: "@you:matrix.org", - StateKey: ptr("Ì£@tulir:maunium.net"), - Content: mautrix.Content{ - Membership: mautrix.MembershipInvite, - }, - } - leaveEvt := &mautrix.Event{ - ID: "!leave:matrix.org", - Type: mautrix.StateMember, - Sender: "@you:matrix.org", - StateKey: ptr("Ì£@tulir:maunium.net"), - Content: mautrix.Content{ - Membership: mautrix.MembershipLeave, - }, - } - - resp := newRespSync() - resp.Rooms.Join["!foo:maunium.net"] = join{ - State: events{Events: []*mautrix.Event{joinEvt}}, - Timeline: timeline{Events: []*mautrix.Event{messageEvt, unhandledEvt}}, - } - resp.Rooms.Invite["!bar:maunium.net"] = struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"invite_state"` - }{ - State: events{Events: []*mautrix.Event{inviteEvt}}, - } - resp.Rooms.Leave["!test:maunium.net"] = struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - }{ - State: events{Events: []*mautrix.Event{leaveEvt}}, - } - - syncer.ProcessResponse(resp, "since") - assert.Contains(t, ml.received, joinEvt, joinEvt.ID) - assert.Contains(t, ml.received, messageEvt, messageEvt.ID) - assert.NotContains(t, ml.received, unhandledEvt, unhandledEvt.ID) - assert.Contains(t, ml.received, inviteEvt, inviteEvt.ID) - assert.Contains(t, ml.received, leaveEvt, leaveEvt.ID) -} - -type mockSyncerSession struct { - rooms map[string]*rooms.Room - userID string -} - -func (mss *mockSyncerSession) GetRoom(id string) *rooms.Room { - return mss.rooms[id] -} - -func (mss *mockSyncerSession) GetUserID() string { - return mss.userID -} - -type events struct { - Events []*mautrix.Event `json:"events"` -} - -type timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` -} -type join struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - Ephemeral struct { - Events []*mautrix.Event `json:"events"` - } `json:"ephemeral"` - AccountData struct { - Events []*mautrix.Event `json:"events"` - } `json:"account_data"` -} - -func ptr(text string) *string { - return &text -} - -type mockListener struct { - received []*mautrix.Event -} - -func (ml *mockListener) receive(source matrix.EventSource, evt *mautrix.Event) { - ml.received = append(ml.received, evt) -} - -func newRespSync() *mautrix.RespSync { - resp := &mautrix.RespSync{NextBatch: "123"} - resp.Rooms.Join = make(map[string]struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - Ephemeral struct { - Events []*mautrix.Event `json:"events"` - } `json:"ephemeral"` - AccountData struct { - Events []*mautrix.Event `json:"events"` - } `json:"account_data"` - }) - resp.Rooms.Invite = make(map[string]struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"invite_state"` - }) - resp.Rooms.Leave = make(map[string]struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - }) - return resp -} diff --git a/ui/commands.go b/ui/commands.go index a74a164..ba13fc3 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -35,14 +35,16 @@ import ( "github.com/russross/blackfriday/v2" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/debug" ) func cmdMe(cmd *Command) { text := strings.Join(cmd.Args, " ") - go cmd.Room.SendMessage(mautrix.MsgEmote, text) + go cmd.Room.SendMessage(event.MsgEmote, text) } // GradientTable from https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go @@ -79,7 +81,7 @@ var rainbow = GradientTable{ } // TODO this command definitely belongs in a plugin once we have a plugin system. -func makeRainbow(cmd *Command, msgtype mautrix.MessageType) { +func makeRainbow(cmd *Command, msgtype event.MessageType) { text := strings.Join(cmd.Args, " ") render := NewRainbowRenderer(blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ @@ -101,15 +103,15 @@ func makeRainbow(cmd *Command, msgtype mautrix.MessageType) { } func cmdRainbow(cmd *Command) { - makeRainbow(cmd, mautrix.MsgText) + makeRainbow(cmd, event.MsgText) } func cmdRainbowMe(cmd *Command) { - makeRainbow(cmd, mautrix.MsgEmote) + makeRainbow(cmd, event.MsgEmote) } func cmdNotice(cmd *Command) { - go cmd.Room.SendMessage(mautrix.MsgNotice, strings.Join(cmd.Args, " ")) + go cmd.Room.SendMessage(event.MsgNotice, strings.Join(cmd.Args, " ")) } func cmdAccept(cmd *Command) { @@ -118,7 +120,7 @@ func cmdAccept(cmd *Command) { cmd.Reply("/accept can only be used in rooms you're invited to") return } - _, server, _ := mautrix.ParseUserID(room.SessionMember.Sender) + _, server, _ := room.SessionMember.Sender.Parse() _, err := cmd.Matrix.JoinRoom(room.ID, server) if err != nil { cmd.Reply("Failed to accept invite:", err) @@ -223,11 +225,11 @@ func cmdTag(cmd *Command) { } var err error if len(cmd.Args) > 2 && cmd.Args[2] == "--reset" { - tags := mautrix.Tags{ + tags := event.Tags{ cmd.Args[0]: {Order: json.Number(fmt.Sprintf("%f", order))}, } for _, tag := range cmd.Room.MxRoom().RawTags { - tags[tag.Tag] = mautrix.Tag{Order: tag.Order} + tags[tag.Tag] = event.Tag{Order: tag.Order} } err = cmd.Matrix.Client().SetTags(cmd.Room.MxRoom().ID, tags) } else { @@ -253,7 +255,7 @@ func cmdRoomNick(cmd *Command) { room := cmd.Room.MxRoom() member := room.GetMember(room.SessionUserID) member.Displayname = strings.Join(cmd.Args, " ") - _, err := cmd.Matrix.Client().SendStateEvent(room.ID, mautrix.StateMember, room.SessionUserID, member) + _, err := cmd.Matrix.Client().SendStateEvent(room.ID, event.StateMember, string(room.SessionUserID), member) if err != nil { cmd.Reply("Failed to set room nick:", err) } @@ -376,7 +378,7 @@ func cmdInvite(cmd *Command) { cmd.Reply("Usage: /invite <user id>") return } - _, err := cmd.Matrix.Client().InviteUser(cmd.Room.MxRoom().ID, &mautrix.ReqInviteUser{UserID: cmd.Args[0]}) + _, err := cmd.Matrix.Client().InviteUser(cmd.Room.MxRoom().ID, &mautrix.ReqInviteUser{UserID: id.UserID(cmd.Args[0])}) if err != nil { debug.Print("Error in invite call:", err) cmd.Reply("Failed to invite user: %v", err) @@ -392,7 +394,7 @@ func cmdBan(cmd *Command) { if len(cmd.Args) >= 2 { reason = strings.Join(cmd.Args[1:], " ") } - _, err := cmd.Matrix.Client().BanUser(cmd.Room.MxRoom().ID, &mautrix.ReqBanUser{Reason: reason, UserID: cmd.Args[0]}) + _, err := cmd.Matrix.Client().BanUser(cmd.Room.MxRoom().ID, &mautrix.ReqBanUser{Reason: reason, UserID: id.UserID(cmd.Args[0])}) if err != nil { debug.Print("Error in ban call:", err) cmd.Reply("Failed to ban user: %v", err) @@ -405,7 +407,7 @@ func cmdUnban(cmd *Command) { cmd.Reply("Usage: /unban <user>") return } - _, err := cmd.Matrix.Client().UnbanUser(cmd.Room.MxRoom().ID, &mautrix.ReqUnbanUser{UserID: cmd.Args[0]}) + _, err := cmd.Matrix.Client().UnbanUser(cmd.Room.MxRoom().ID, &mautrix.ReqUnbanUser{UserID: id.UserID(cmd.Args[0])}) if err != nil { debug.Print("Error in unban call:", err) cmd.Reply("Failed to unban user: %v", err) @@ -421,7 +423,7 @@ func cmdKick(cmd *Command) { if len(cmd.Args) >= 2 { reason = strings.Join(cmd.Args[1:], " ") } - _, err := cmd.Matrix.Client().KickUser(cmd.Room.MxRoom().ID, &mautrix.ReqKickUser{Reason: reason, UserID: cmd.Args[0]}) + _, err := cmd.Matrix.Client().KickUser(cmd.Room.MxRoom().ID, &mautrix.ReqKickUser{Reason: reason, UserID: id.UserID(cmd.Args[0])}) if err != nil { debug.Print("Error in kick call:", err) debug.Print("Failed to kick user:", err) @@ -445,9 +447,18 @@ func cmdPrivateMessage(cmd *Command) { if len(cmd.Args) == 0 { cmd.Reply("Usage: /pm <user id> [more user ids...]") } + invites := make([]id.UserID, len(cmd.Args)) + for i, userID := range cmd.Args { + invites[i] = id.UserID(userID) + _, _, err := invites[i].Parse() + if err != nil { + cmd.Reply("%s isn't a valid user ID", userID) + return + } + } req := &mautrix.ReqCreateRoom{ Preset: "trusted_private_chat", - Invite: cmd.Args, + Invite: invites, } room, err := cmd.Matrix.CreateRoom(req) if err != nil { @@ -462,7 +473,7 @@ func cmdJoin(cmd *Command) { cmd.Reply("Usage: /join <room>") return } - identifer := cmd.Args[0] + identifer := id.RoomID(cmd.Args[0]) server := "" if len(cmd.Args) > 1 { server = cmd.Args[1] @@ -479,7 +490,7 @@ func cmdMSendEvent(cmd *Command) { cmd.Reply("Usage: /msend <event type> <content>") return } - cmd.Args = append([]string{cmd.Room.MxRoom().ID}, cmd.Args...) + cmd.Args = append([]string{string(cmd.Room.MxRoom().ID)}, cmd.Args...) cmdSendEvent(cmd) } @@ -488,8 +499,8 @@ func cmdSendEvent(cmd *Command) { cmd.Reply("Usage: /send <room id> <event type> <content>") return } - roomID := cmd.Args[0] - eventType := mautrix.NewEventType(cmd.Args[1]) + roomID := id.RoomID(cmd.Args[0]) + eventType := event.NewEventType(cmd.Args[1]) rawContent := strings.Join(cmd.Args[2:], " ") var content interface{} @@ -515,7 +526,7 @@ func cmdMSetState(cmd *Command) { cmd.Reply("Usage: /msetstate <event type> <state key> <content>") return } - cmd.Args = append([]string{cmd.Room.MxRoom().ID}, cmd.Args...) + cmd.Args = append([]string{string(cmd.Room.MxRoom().ID)}, cmd.Args...) cmdSetState(cmd) } @@ -525,8 +536,8 @@ func cmdSetState(cmd *Command) { return } - roomID := cmd.Args[0] - eventType := mautrix.NewEventType(cmd.Args[1]) + roomID := id.RoomID(cmd.Args[0]) + eventType := event.NewEventType(cmd.Args[1]) stateKey := cmd.Args[2] if stateKey == "-" { stateKey = "" diff --git a/ui/fuzzy-search-modal.go b/ui/fuzzy-search-modal.go index 9f91877..bf9004b 100644 --- a/ui/fuzzy-search-modal.go +++ b/ui/fuzzy-search-modal.go @@ -23,6 +23,7 @@ import ( "github.com/lithammer/fuzzysearch/fuzzy" + "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -87,7 +88,7 @@ func (fs *FuzzySearchModal) Blur() { fs.container.Blur() } -func (fs *FuzzySearchModal) InitList(rooms map[string]*RoomView) { +func (fs *FuzzySearchModal) InitList(rooms map[id.RoomID]*RoomView) { for _, room := range rooms { if room.Room.IsReplaced() { //if _, ok := rooms[room.Room.ReplacedBy()]; ok diff --git a/ui/member-list.go b/ui/member-list.go index e836e18..b47840e 100644 --- a/ui/member-list.go +++ b/ui/member-list.go @@ -23,7 +23,8 @@ import ( "github.com/mattn/go-runewidth" - "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -43,7 +44,7 @@ type memberListItem struct { rooms.Member PowerLevel int Sigil rune - UserID string + UserID id.UserID Color tcell.Color } @@ -64,7 +65,7 @@ func (rml roomMemberList) Swap(i, j int) { rml[i], rml[j] = rml[j], rml[i] } -func (ml *MemberList) Update(data map[string]*rooms.Member, levels *mautrix.PowerLevels) *MemberList { +func (ml *MemberList) Update(data map[id.UserID]*rooms.Member, levels *event.PowerLevels) *MemberList { ml.list = make(roomMemberList, len(data)) i := 0 highestLevel := math.MinInt32 diff --git a/ui/message-view.go b/ui/message-view.go index 4f406e1..764143f 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -25,10 +25,12 @@ import ( "github.com/mattn/go-runewidth" sync "github.com/sasha-s/go-deadlock" - "maunium.net/go/mautrix" "maunium.net/go/mauview" "maunium.net/go/tcell" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" @@ -59,7 +61,7 @@ type MessageView struct { prevPrefs config.UserPreferences messageIDLock sync.RWMutex - messageIDs map[string]*messages.UIMessage + messageIDs map[id.EventID]*messages.UIMessage messagesLock sync.RWMutex messages []*messages.UIMessage msgBufferLock sync.RWMutex @@ -79,7 +81,7 @@ func NewMessageView(parent *RoomView) *MessageView { ScrollOffset: 0, messages: make([]*messages.UIMessage, 0), - messageIDs: make(map[string]*messages.UIMessage), + messageIDs: make(map[id.EventID]*messages.UIMessage), msgBuffer: make([]*messages.UIMessage, 0), _width: 80, @@ -95,7 +97,7 @@ func (view *MessageView) Unload() { view.messagesLock.Lock() view.msgBufferLock.Lock() view.messageIDLock.Lock() - view.messageIDs = make(map[string]*messages.UIMessage) + view.messageIDs = make(map[id.EventID]*messages.UIMessage) view.msgBuffer = make([]*messages.UIMessage, 0) view.messages = make([]*messages.UIMessage, 0) view.initialHistoryLoaded = false @@ -140,9 +142,9 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil { view.replaceMessage(oldMsg, message) direction = IgnoreMessage - } else if oldMsg = view.getMessageByID(message.TxnID); oldMsg != nil { + } else if oldMsg = view.getMessageByID(id.EventID(message.TxnID)); oldMsg != nil { view.replaceMessage(oldMsg, message) - view.deleteMessageID(message.TxnID) + view.deleteMessageID(id.EventID(message.TxnID)) direction = IgnoreMessage } @@ -208,7 +210,7 @@ func (view *MessageView) replaceMessage(original *messages.UIMessage, new *messa view.messagesLock.Unlock() } -func (view *MessageView) getMessageByID(id string) *messages.UIMessage { +func (view *MessageView) getMessageByID(id id.EventID) *messages.UIMessage { if id == "" { return nil } @@ -221,7 +223,7 @@ func (view *MessageView) getMessageByID(id string) *messages.UIMessage { return msg } -func (view *MessageView) deleteMessageID(id string) { +func (view *MessageView) deleteMessageID(id id.EventID) { if id == "" { return } @@ -365,7 +367,7 @@ func (view *MessageView) handleUsernameClick(message *messages.UIMessage, prevMe // return false //} - if message.SenderName == "---" || message.SenderName == "-->" || message.SenderName == "<--" || message.Type == mautrix.MsgEmote { + if message.SenderName == "---" || message.SenderName == "-->" || message.SenderName == "<--" || message.Type == event.MsgEmote { return false } @@ -564,7 +566,7 @@ func (view *MessageView) CapturePlaintext(height int) string { var sender string if len(message.Sender()) > 0 { sender = fmt.Sprintf(" <%s>", message.Sender()) - } else if message.Type == mautrix.MsgEmote { + } else if message.Type == event.MsgEmote { sender = fmt.Sprintf(" * %s", message.SenderName) } fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText()) diff --git a/ui/messages/base.go b/ui/messages/base.go index 745bfcd..eaeb552 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -22,8 +22,9 @@ import ( "time" "maunium.net/go/gomuks/config" - "maunium.net/go/gomuks/matrix/event" - "maunium.net/go/mautrix" + "maunium.net/go/gomuks/matrix/muksevt" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -64,26 +65,26 @@ func (rs ReactionSlice) Swap(i, j int) { } type UIMessage struct { - EventID string + EventID id.EventID TxnID string - Relation mautrix.RelatesTo - Type mautrix.MessageType - SenderID string + Relation event.RelatesTo + Type event.MessageType + SenderID id.UserID SenderName string DefaultSenderColor tcell.Color Timestamp time.Time - State event.OutgoingState + State muksevt.OutgoingState IsHighlight bool IsService bool IsSelected bool Edited bool - Event *event.Event + Event *muksevt.Event ReplyTo *UIMessage Reactions ReactionSlice Renderer MessageRenderer } -func (msg *UIMessage) GetEvent() *event.Event { +func (msg *UIMessage) GetEvent() *muksevt.Event { if msg == nil { return nil } @@ -93,10 +94,10 @@ func (msg *UIMessage) GetEvent() *event.Event { const DateFormat = "January _2, 2006" const TimeFormat = "15:04:05" -func newUIMessage(evt *event.Event, displayname string, renderer MessageRenderer) *UIMessage { +func newUIMessage(evt *muksevt.Event, displayname string, renderer MessageRenderer) *UIMessage { msgtype := evt.Content.MsgType if len(msgtype) == 0 { - msgtype = mautrix.MessageType(evt.Type.String()) + msgtype = event.MessageType(evt.Type.String()) } reactions := make(ReactionSlice, 0, len(evt.Unsigned.Relations.Annotations.Map)) @@ -161,9 +162,9 @@ func unixToTime(unix int64) time.Time { // In any other case, the sender is the display name of the user who sent the message. func (msg *UIMessage) Sender() string { switch msg.State { - case event.StateLocalEcho: + case muksevt.StateLocalEcho: return "Sending..." - case event.StateSendFail: + case muksevt.StateSendFail: return "Error" } switch msg.Type { @@ -185,11 +186,11 @@ func (msg *UIMessage) NotificationContent() string { func (msg *UIMessage) getStateSpecificColor() tcell.Color { switch msg.State { - case event.StateLocalEcho: + case muksevt.StateLocalEcho: return tcell.ColorGray - case event.StateSendFail: + case muksevt.StateSendFail: return tcell.ColorRed - case event.StateDefault: + case muksevt.StateDefault: fallthrough default: return tcell.ColorDefault @@ -286,14 +287,14 @@ func (msg *UIMessage) SameDate(message *UIMessage) bool { return day1 == day2 && month1 == month2 && year1 == year2 } -func (msg *UIMessage) ID() string { +func (msg *UIMessage) ID() id.EventID { if len(msg.EventID) == 0 { - return msg.TxnID + return id.EventID(msg.TxnID) } return msg.EventID } -func (msg *UIMessage) SetID(id string) { +func (msg *UIMessage) SetID(id id.EventID) { msg.EventID = id } diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index bd5aba9..5c9b5bd 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -20,7 +20,7 @@ import ( "fmt" "time" - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -34,7 +34,7 @@ type ExpandedTextMessage struct { } // NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. -func NewExpandedTextMessage(evt *event.Event, displayname string, text tstring.TString) *UIMessage { +func NewExpandedTextMessage(evt *muksevt.Event, displayname string, text tstring.TString) *UIMessage { return newUIMessage(evt, displayname, &ExpandedTextMessage{ Text: text, }) diff --git a/ui/messages/filemessage.go b/ui/messages/filemessage.go index daf6c00..baffd0f 100644 --- a/ui/messages/filemessage.go +++ b/ui/messages/filemessage.go @@ -22,8 +22,9 @@ import ( "image" "image/color" - "maunium.net/go/gomuks/matrix/event" - "maunium.net/go/mautrix" + "maunium.net/go/gomuks/matrix/muksevt" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -35,10 +36,10 @@ import ( ) type FileMessage struct { - Type mautrix.MessageType + Type event.MessageType Body string - URL mautrix.ContentURI - Thumbnail mautrix.ContentURI + URL id.ContentURI + Thumbnail id.ContentURI imageData []byte buffer []tstring.TString @@ -46,9 +47,9 @@ type FileMessage struct { } // NewFileMessage creates a new FileMessage object with the provided values and the default state. -func NewFileMessage(matrix ifc.MatrixContainer, evt *event.Event, displayname string) *UIMessage { - url, _ := mautrix.ParseContentURI(evt.Content.URL) - thumbnail, _ := mautrix.ParseContentURI(evt.Content.GetInfo().ThumbnailURL) +func NewFileMessage(matrix ifc.MatrixContainer, evt *muksevt.Event, displayname string) *UIMessage { + url, _ := evt.Content.URL.Parse() + thumbnail, _ := evt.Content.GetInfo().ThumbnailURL.Parse() return newUIMessage(evt, displayname, &FileMessage{ Type: evt.Content.MsgType, Body: evt.Content.Body, @@ -72,13 +73,13 @@ func (msg *FileMessage) Clone() MessageRenderer { func (msg *FileMessage) NotificationContent() string { switch msg.Type { - case mautrix.MsgImage: + case event.MsgImage: return "Sent an image" - case mautrix.MsgAudio: + case event.MsgAudio: return "Sent an audio file" - case mautrix.MsgVideo: + case event.MsgVideo: return "Sent a video" - case mautrix.MsgFile: + case event.MsgFile: fallthrough default: return "Sent a file" @@ -96,7 +97,7 @@ func (msg *FileMessage) String() string { func (msg *FileMessage) DownloadPreview() { url := msg.Thumbnail if url.IsEmpty() { - if msg.Type == mautrix.MsgImage && !msg.URL.IsEmpty() { + if msg.Type == event.MsgImage && !msg.URL.IsEmpty() { msg.Thumbnail = msg.URL url = msg.Thumbnail } else { diff --git a/ui/messages/html/parser.go b/ui/messages/html/parser.go index 99f9c29..86b501d 100644 --- a/ui/messages/html/parser.go +++ b/ui/messages/html/parser.go @@ -27,8 +27,9 @@ import ( "github.com/lucasb-eyer/go-colorful" "golang.org/x/net/html" - "maunium.net/go/gomuks/matrix/event" - "maunium.net/go/mautrix" + "maunium.net/go/gomuks/matrix/muksevt" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/tcell" "maunium.net/go/gomuks/matrix/rooms" @@ -185,12 +186,12 @@ func (parser *htmlParser) linkToEntity(node *html.Node) Entity { pillTarget := match[1] text := NewTextEntity(pillTarget) if pillTarget[0] == '@' { - if member := parser.room.GetMember(pillTarget); member != nil { + if member := parser.room.GetMember(id.UserID(pillTarget)); member != nil { text.Text = member.Displayname text.Style = text.Style.Foreground(widget.GetHashColor(pillTarget)) } entity.Children = []Entity{text} - /*} else if slash := strings.IndexRune(pillTarget, '/'); slash != -1 { + /*} else if slash := strings.IndexRune(pillTarget, '/'); slash != -1 { room := pillTarget[:slash] event := pillTarget[slash+1:]*/ } else if pillTarget[0] == '#' { @@ -383,9 +384,9 @@ func (parser *htmlParser) Parse(htmlData string) Entity { const TabLength = 4 // Parse parses a HTML-formatted Matrix event into a UIMessage. -func Parse(room *rooms.Room, evt *event.Event, senderDisplayname string) Entity { +func Parse(room *rooms.Room, evt *muksevt.Event, senderDisplayname string) Entity { htmlData := evt.Content.FormattedBody - if evt.Content.Format != mautrix.FormatHTML { + if evt.Content.Format != event.FormatHTML { htmlData = strings.Replace(html.EscapeString(evt.Content.Body), "\n", "<br/>", -1) } htmlData = strings.Replace(htmlData, "\t", strings.Repeat(" ", TabLength), -1) @@ -402,7 +403,7 @@ func Parse(room *rooms.Room, evt *event.Event, senderDisplayname string) Entity } } - if evt.Content.MsgType == mautrix.MsgEmote { + if evt.Content.MsgType == event.MsgEmote { root = &ContainerEntity{ BaseEntity: &BaseEntity{ Tag: "emote", diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go index af3ce41..cc206fd 100644 --- a/ui/messages/htmlmessage.go +++ b/ui/messages/htmlmessage.go @@ -17,7 +17,7 @@ package messages import ( - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -32,7 +32,7 @@ type HTMLMessage struct { focused bool } -func NewHTMLMessage(evt *event.Event, displayname string, root html.Entity) *UIMessage { +func NewHTMLMessage(evt *muksevt.Event, displayname string, root html.Entity) *UIMessage { return newUIMessage(evt, displayname, &HTMLMessage{ Root: root, }) diff --git a/ui/messages/parser.go b/ui/messages/parser.go index d0bc6e0..d89bcb9 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -20,8 +20,9 @@ import ( "fmt" "strings" - "maunium.net/go/gomuks/matrix/event" - "maunium.net/go/mautrix" + "maunium.net/go/gomuks/matrix/muksevt" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" @@ -31,7 +32,7 @@ import ( "maunium.net/go/gomuks/ui/widget" ) -func getCachedEvent(mainView ifc.MainView, roomID, eventID string) *UIMessage { +func getCachedEvent(mainView ifc.MainView, roomID id.RoomID, eventID id.EventID) *UIMessage { if roomView := mainView.GetRoom(roomID); roomView != nil { if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil { if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil { @@ -42,7 +43,7 @@ func getCachedEvent(mainView ifc.MainView, roomID, eventID string) *UIMessage { return nil } -func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *event.Event) *UIMessage { +func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *muksevt.Event) *UIMessage { msg := directParseEvent(matrix, room, evt) if msg == nil { return nil @@ -64,36 +65,36 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R return msg } -func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event) *UIMessage { - displayname := evt.Sender +func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *muksevt.Event) *UIMessage { + displayname := string(evt.Sender) member := room.GetMember(evt.Sender) if member != nil { displayname = member.Displayname } - if evt.Unsigned.RedactedBecause != nil || evt.Type == mautrix.EventRedaction { + if evt.Unsigned.RedactedBecause != nil || evt.Type == event.EventRedaction { return NewRedactedMessage(evt, displayname) } switch evt.Type { - case mautrix.EventSticker: - evt.Content.MsgType = mautrix.MsgImage + case event.EventSticker: + evt.Content.MsgType = event.MsgImage fallthrough - case mautrix.EventMessage: + case event.EventMessage: return ParseMessage(matrix, room, evt, displayname) - case mautrix.EventEncrypted: + case event.EventEncrypted: return NewExpandedTextMessage(evt, displayname, tstring.NewStyleTString("Encrypted messages are not yet supported", tcell.StyleDefault.Italic(true))) - case mautrix.StateTopic, mautrix.StateRoomName, mautrix.StateAliases, mautrix.StateCanonicalAlias: + case event.StateTopic, event.StateRoomName, event.StateAliases, event.StateCanonicalAlias: return ParseStateEvent(evt, displayname) - case mautrix.StateMember: + case event.StateMember: return ParseMembershipEvent(room, evt) } return nil } -func ParseStateEvent(evt *event.Event, displayname string) *UIMessage { +func ParseStateEvent(evt *muksevt.Event, displayname string) *UIMessage { text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender)) switch evt.Type { - case mautrix.StateTopic: + case event.StateTopic: if len(evt.Content.Topic) == 0 { text = text.AppendColor(" removed the topic.", tcell.ColorGreen) } else { @@ -101,7 +102,7 @@ func ParseStateEvent(evt *event.Event, displayname string) *UIMessage { AppendStyle(evt.Content.Topic, tcell.StyleDefault.Underline(true)). AppendColor(".", tcell.ColorGreen) } - case mautrix.StateRoomName: + case event.StateRoomName: if len(evt.Content.Name) == 0 { text = text.AppendColor(" removed the room name.", tcell.ColorGreen) } else { @@ -109,21 +110,21 @@ func ParseStateEvent(evt *event.Event, displayname string) *UIMessage { AppendStyle(evt.Content.Name, tcell.StyleDefault.Underline(true)). AppendColor(".", tcell.ColorGreen) } - case mautrix.StateCanonicalAlias: + case event.StateCanonicalAlias: if len(evt.Content.Alias) == 0 { text = text.AppendColor(" removed the main address of the room.", tcell.ColorGreen) } else { text = text.AppendColor(" changed the main address of the room to ", tcell.ColorGreen). - AppendStyle(evt.Content.Alias, tcell.StyleDefault.Underline(true)). + AppendStyle(string(evt.Content.Alias), tcell.StyleDefault.Underline(true)). AppendColor(".", tcell.ColorGreen) } - case mautrix.StateAliases: - text = ParseAliasEvent(evt, displayname) + //case event.StateAliases: + // text = ParseAliasEvent(evt, displayname) } return NewExpandedTextMessage(evt, displayname, text) } -func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event, displayname string) *UIMessage { +func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *muksevt.Event, displayname string) *UIMessage { if len(evt.Content.GetReplyTo()) > 0 { evt.Content.RemoveReplyFallback() } @@ -131,13 +132,13 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event evt.Content = *evt.Gomuks.Edits[len(evt.Gomuks.Edits)-1].Content.NewContent } switch evt.Content.MsgType { - case mautrix.MsgText, mautrix.MsgNotice, mautrix.MsgEmote: - if evt.Content.Format == mautrix.FormatHTML { + case event.MsgText, event.MsgNotice, event.MsgEmote: + if evt.Content.Format == event.FormatHTML { return NewHTMLMessage(evt, displayname, html.Parse(room, evt, displayname)) } evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1) return NewTextMessage(evt, displayname, evt.Content.Body) - case mautrix.MsgImage, mautrix.MsgVideo, mautrix.MsgAudio, mautrix.MsgFile: + case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile: msg := NewFileMessage(matrix, evt, displayname) if !matrix.Preferences().DisableDownloads { renderer := msg.Renderer.(*FileMessage) @@ -148,54 +149,54 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event return nil } -func getMembershipChangeMessage(evt *event.Event, membership, prevMembership mautrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { +func getMembershipChangeMessage(evt *muksevt.Event, membership, prevMembership event.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { switch membership { case "invite": sender = "---" text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", senderDisplayname, displayname), tcell.ColorGreen) text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) - text.Colorize(len(senderDisplayname)+len(" invited "), len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(len(senderDisplayname)+len(" invited "), len(displayname), widget.GetHashColor(evt.StateKey)) case "join": sender = "-->" - if prevMembership == mautrix.MembershipInvite { + if prevMembership == event.MembershipInvite { text = tstring.NewColorTString(fmt.Sprintf("%s accepted the invite.", displayname), tcell.ColorGreen) } else { text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) } - text.Colorize(0, len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(0, len(displayname), widget.GetHashColor(evt.StateKey)) case "leave": sender = "<--" - if evt.Sender != *evt.StateKey { - if prevMembership == mautrix.MembershipBan { + if evt.Sender != id.UserID(*evt.StateKey) { + if prevMembership == event.MembershipBan { text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen) - text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(evt.StateKey)) } else { text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) - text.Colorize(len(senderDisplayname)+len(" kicked "), len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(len(senderDisplayname)+len(" kicked "), len(displayname), widget.GetHashColor(evt.StateKey)) } text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) } else { if displayname == *evt.StateKey { displayname = prevDisplayname } - if prevMembership == mautrix.MembershipInvite { + if prevMembership == event.MembershipInvite { text = tstring.NewColorTString(fmt.Sprintf("%s rejected the invite.", displayname), tcell.ColorRed) } else { text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) } - text.Colorize(0, len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(0, len(displayname), widget.GetHashColor(evt.StateKey)) } case "ban": text = tstring.NewColorTString(fmt.Sprintf("%s banned %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) - text.Colorize(len(senderDisplayname)+len(" banned "), len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(len(senderDisplayname)+len(" banned "), len(displayname), widget.GetHashColor(evt.StateKey)) text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) } return } -func getMembershipEventContent(room *rooms.Room, evt *event.Event) (sender string, text tstring.TString) { +func getMembershipEventContent(room *rooms.Room, evt *muksevt.Event) (sender string, text tstring.TString) { member := room.GetMember(evt.Sender) - senderDisplayname := evt.Sender + senderDisplayname := string(evt.Sender) if member != nil { senderDisplayname = member.Displayname } @@ -206,7 +207,7 @@ func getMembershipEventContent(room *rooms.Room, evt *event.Event) (sender strin displayname = *evt.StateKey } - prevMembership := mautrix.MembershipLeave + prevMembership := event.MembershipLeave prevDisplayname := *evt.StateKey if evt.Unsigned.PrevContent != nil { prevMembership = evt.Unsigned.PrevContent.Membership @@ -220,7 +221,7 @@ func getMembershipEventContent(room *rooms.Room, evt *event.Event) (sender strin sender, text = getMembershipChangeMessage(evt, membership, prevMembership, senderDisplayname, displayname, prevDisplayname) } else if displayname != prevDisplayname { sender = "---" - color := widget.GetHashColor(*evt.StateKey) + color := widget.GetHashColor(evt.StateKey) text = tstring.NewBlankTString(). AppendColor(prevDisplayname, color). AppendColor(" changed their display name to ", tcell.ColorGreen). @@ -230,7 +231,7 @@ func getMembershipEventContent(room *rooms.Room, evt *event.Event) (sender strin return } -func ParseMembershipEvent(room *rooms.Room, evt *event.Event) *UIMessage { +func ParseMembershipEvent(room *rooms.Room, evt *muksevt.Event) *UIMessage { displayname, text := getMembershipEventContent(room, evt) if len(text) == 0 { return nil @@ -239,65 +240,65 @@ func ParseMembershipEvent(room *rooms.Room, evt *event.Event) *UIMessage { return NewExpandedTextMessage(evt, displayname, text) } -func ParseAliasEvent(evt *event.Event, displayname string) tstring.TString { - var prevAliases []string - if evt.Unsigned.PrevContent != nil { - prevAliases = evt.Unsigned.PrevContent.Aliases - } - aliases := evt.Content.Aliases - var added, removed []tstring.TString -Outer1: - for _, oldAlias := range prevAliases { - for _, newAlias := range aliases { - if oldAlias == newAlias { - continue Outer1 - } - } - removed = append(removed, tstring.NewStyleTString(oldAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true))) - } -Outer2: - for _, newAlias := range aliases { - for _, oldAlias := range prevAliases { - if oldAlias == newAlias { - continue Outer2 - } - } - added = append(added, tstring.NewStyleTString(newAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true))) - } - var addedStr, removedStr tstring.TString - if len(added) == 1 { - addedStr = added[0] - } else if len(added) > 1 { - addedStr = tstring. - Join(added[:len(added)-1], ", "). - Append(" and "). - AppendTString(added[len(added)-1]) - } - if len(removed) == 1 { - removedStr = removed[0] - } else if len(removed) > 1 { - removedStr = tstring. - Join(removed[:len(removed)-1], ", "). - Append(" and "). - AppendTString(removed[len(removed)-1]) - } - text := tstring.NewBlankTString() - if len(addedStr) > 0 && len(removedStr) > 0 { - text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). - AppendTString(addedStr). - AppendColor(" and removed ", tcell.ColorGreen). - AppendTString(removedStr). - AppendColor(" as addresses for this room.", tcell.ColorGreen) - } else if len(addedStr) > 0 { - text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). - AppendTString(addedStr). - AppendColor(" as addresses for this room.", tcell.ColorGreen) - } else if len(removedStr) > 0 { - text = text.AppendColor(fmt.Sprintf("%s removed ", displayname), tcell.ColorGreen). - AppendTString(removedStr). - AppendColor(" as addresses for this room.", tcell.ColorGreen) - } else { - return nil - } - return text -} +//func ParseAliasEvent(evt *muksevt.Event, displayname string) tstring.TString { +// var prevAliases []string +// if evt.Unsigned.PrevContent != nil { +// prevAliases = evt.Unsigned.PrevContent.Aliases +// } +// aliases := evt.Content.Aliases +// var added, removed []tstring.TString +//Outer1: +// for _, oldAlias := range prevAliases { +// for _, newAlias := range aliases { +// if oldAlias == newAlias { +// continue Outer1 +// } +// } +// removed = append(removed, tstring.NewStyleTString(oldAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true))) +// } +//Outer2: +// for _, newAlias := range aliases { +// for _, oldAlias := range prevAliases { +// if oldAlias == newAlias { +// continue Outer2 +// } +// } +// added = append(added, tstring.NewStyleTString(newAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true))) +// } +// var addedStr, removedStr tstring.TString +// if len(added) == 1 { +// addedStr = added[0] +// } else if len(added) > 1 { +// addedStr = tstring. +// Join(added[:len(added)-1], ", "). +// Append(" and "). +// AppendTString(added[len(added)-1]) +// } +// if len(removed) == 1 { +// removedStr = removed[0] +// } else if len(removed) > 1 { +// removedStr = tstring. +// Join(removed[:len(removed)-1], ", "). +// Append(" and "). +// AppendTString(removed[len(removed)-1]) +// } +// text := tstring.NewBlankTString() +// if len(addedStr) > 0 && len(removedStr) > 0 { +// text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). +// AppendTString(addedStr). +// AppendColor(" and removed ", tcell.ColorGreen). +// AppendTString(removedStr). +// AppendColor(" as addresses for this room.", tcell.ColorGreen) +// } else if len(addedStr) > 0 { +// text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). +// AppendTString(addedStr). +// AppendColor(" as addresses for this room.", tcell.ColorGreen) +// } else if len(removedStr) > 0 { +// text = text.AppendColor(fmt.Sprintf("%s removed ", displayname), tcell.ColorGreen). +// AppendTString(removedStr). +// AppendColor(" as addresses for this room.", tcell.ColorGreen) +// } else { +// return nil +// } +// return text +//} diff --git a/ui/messages/redactedmessage.go b/ui/messages/redactedmessage.go index 34d880b..ac79008 100644 --- a/ui/messages/redactedmessage.go +++ b/ui/messages/redactedmessage.go @@ -17,7 +17,7 @@ package messages import ( - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -26,7 +26,7 @@ import ( type RedactedMessage struct{} -func NewRedactedMessage(evt *event.Event, displayname string) *UIMessage { +func NewRedactedMessage(evt *muksevt.Event, displayname string) *UIMessage { return newUIMessage(evt, displayname, &RedactedMessage{}) } diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 2e59c8f..cf3b590 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -20,7 +20,7 @@ import ( "fmt" "time" - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/mauview" "maunium.net/go/gomuks/config" @@ -35,7 +35,7 @@ type TextMessage struct { } // NewTextMessage creates a new UITextMessage object with the provided values and the default state. -func NewTextMessage(evt *event.Event, displayname string, text string) *UIMessage { +func NewTextMessage(evt *muksevt.Event, displayname string, text string) *UIMessage { return newUIMessage(evt, displayname, &TextMessage{ Text: text, }) diff --git a/ui/room-list.go b/ui/room-list.go index df986fe..3169bba 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -24,6 +24,7 @@ import ( sync "github.com/sasha-s/go-deadlock" + "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -105,7 +106,7 @@ func NewRoomList(parent *MainView) *RoomList { return list } -func (list *RoomList) Contains(roomID string) bool { +func (list *RoomList) Contains(roomID id.RoomID) bool { list.RLock() defer list.RUnlock() for _, trl := range list.items { diff --git a/ui/room-view.go b/ui/room-view.go index 3a1d3cb..8408e25 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -26,18 +26,19 @@ import ( "github.com/kyokomi/emoji" "github.com/mattn/go-runewidth" - "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/lib/open" - "maunium.net/go/gomuks/matrix/event" - "maunium.net/go/mauview" + "maunium.net/go/tcell" "maunium.net/go/mautrix" - "maunium.net/go/tcell" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/config" + "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/lib/open" "maunium.net/go/gomuks/lib/util" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/widget" @@ -72,9 +73,9 @@ type RoomView struct { selectReason SelectReason selectContent string - replying *event.Event + replying *muksevt.Event - editing *event.Event + editing *muksevt.Event editMoveText string completions struct { @@ -186,7 +187,7 @@ func (view *RoomView) OnSelect(message *messages.UIMessage) { case SelectReply: view.replying = message.Event if len(view.selectContent) > 0 { - go view.SendMessage(mautrix.MsgText, view.selectContent) + go view.SendMessage(event.MsgText, view.selectContent) } case SelectReact: go view.SendReaction(message.EventID, view.selectContent) @@ -217,7 +218,7 @@ func (view *RoomView) GetStatus() string { buf.WriteString("Editing message - ") } else if view.replying != nil { buf.WriteString("Replying to ") - buf.WriteString(view.replying.Sender) + buf.WriteString(string(view.replying.Sender)) buf.WriteString(" - ") } else if view.selecting { buf.WriteString("Selecting message to ") @@ -235,12 +236,19 @@ func (view *RoomView) GetStatus() string { } if len(view.typing) == 1 { - buf.WriteString("Typing: " + view.typing[0]) + buf.WriteString("Typing: " + string(view.typing[0])) buf.WriteString(" - ") } else if len(view.typing) > 1 { - _, _ = fmt.Fprintf(&buf, - "Typing: %s and %s - ", - strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1]) + buf.WriteString("Typing: ") + for i, userID := range view.typing { + if i == len(view.typing)-1 { + buf.WriteString(" and ") + } else if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(string(userID)) + } + buf.WriteString(" - ") } return strings.TrimSuffix(buf.String(), " - ") @@ -384,15 +392,18 @@ func (view *RoomView) SetCompletions(completions []string) { func (view *RoomView) loadTyping() { for index, user := range view.typing { - member := view.Room.GetMember(user) + member := view.Room.GetMember(id.UserID(user)) if member != nil { view.typing[index] = member.Displayname } } } -func (view *RoomView) SetTyping(users []string) { - view.typing = users +func (view *RoomView) SetTyping(users []id.UserID) { + view.typing = make([]string, len(users)) + for i, user := range users { + view.typing[i] = string(user) + } if view.Room.Loaded() { view.loadTyping() } @@ -406,13 +417,13 @@ type completion struct { func (view *RoomView) autocompleteUser(existingText string) (completions []completion) { textWithoutPrefix := strings.TrimPrefix(existingText, "@") for userID, user := range view.Room.GetMembers() { - if user.Displayname == textWithoutPrefix || userID == existingText { + if user.Displayname == textWithoutPrefix || string(userID) == existingText { // Exact match, return that. - return []completion{{user.Displayname, userID}} + return []completion{{user.Displayname, string(userID)}} } - if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(userID, existingText) { - completions = append(completions, completion{user.Displayname, userID}) + if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) { + completions = append(completions, completion{user.Displayname, string(userID)}) } } return @@ -420,13 +431,13 @@ func (view *RoomView) autocompleteUser(existingText string) (completions []compl func (view *RoomView) autocompleteRoom(existingText string) (completions []completion) { for _, room := range view.parent.rooms { - alias := room.Room.GetCanonicalAlias() + alias := string(room.Room.GetCanonicalAlias()) if alias == existingText { // Exact match, return that. - return []completion{{alias, room.Room.ID}} + return []completion{{alias, string(room.Room.ID)}} } if strings.HasPrefix(alias, existingText) { - completions = append(completions, completion{alias, room.Room.ID}) + completions = append(completions, completion{alias, string(room.Room.ID)}) continue } } @@ -457,7 +468,7 @@ func (view *RoomView) autocompleteEmoji(word string) (completions []string) { return } -func (view *RoomView) SetEditing(evt *event.Event) { +func (view *RoomView) SetEditing(evt *muksevt.Event) { if evt == nil { view.editing = nil view.SetInputText(view.editMoveText) @@ -470,7 +481,7 @@ func (view *RoomView) SetEditing(evt *event.Event) { // replying should never be non-nil when SetEditing, but do this just to be safe view.replying = nil text := view.editing.Content.Body - if view.editing.Content.MsgType == mautrix.MsgEmote { + if view.editing.Content.MsgType == event.MsgEmote { text = "/me " + text } view.input.SetText(text) @@ -479,21 +490,21 @@ func (view *RoomView) SetEditing(evt *event.Event) { view.input.SetCursorOffset(-1) } -type findFilter func(evt *event.Event) bool +type findFilter func(evt *muksevt.Event) bool -func (view *RoomView) filterOwnOnly(evt *event.Event) bool { - return evt.Sender == view.parent.matrix.Client().UserID && evt.Type == mautrix.EventMessage +func (view *RoomView) filterOwnOnly(evt *muksevt.Event) bool { + return evt.Sender == view.parent.matrix.Client().UserID && evt.Type == event.EventMessage } -func (view *RoomView) filterMediaOnly(evt *event.Event) bool { - return evt.Type == mautrix.EventMessage && ( - evt.Content.MsgType == mautrix.MsgFile || - evt.Content.MsgType == mautrix.MsgImage || - evt.Content.MsgType == mautrix.MsgAudio || - evt.Content.MsgType == mautrix.MsgVideo) +func (view *RoomView) filterMediaOnly(evt *muksevt.Event) bool { + return evt.Type == event.EventMessage && ( + evt.Content.MsgType == event.MsgFile || + evt.Content.MsgType == event.MsgImage || + evt.Content.MsgType == event.MsgAudio || + evt.Content.MsgType == event.MsgVideo) } -func (view *RoomView) findMessage(current *event.Event, forward bool, allow findFilter) *messages.UIMessage { +func (view *RoomView) findMessage(current *muksevt.Event, forward bool, allow findFilter) *messages.UIMessage { currentFound := current == nil msgs := view.MessageView().messages for i := 0; i < len(msgs); i++ { @@ -502,7 +513,7 @@ func (view *RoomView) findMessage(current *event.Event, forward bool, allow find index = len(msgs) - i - 1 } evt := msgs[index] - if evt.EventID == "" || evt.EventID == evt.TxnID || evt.IsService { + if evt.EventID == "" || string(evt.EventID) == evt.TxnID || evt.IsService { continue } else if currentFound { if allow == nil || allow(evt.Event) { @@ -607,13 +618,13 @@ func (view *RoomView) InputSubmit(text string) { } else if cmd := view.parent.cmdProcessor.ParseCommand(view, text); cmd != nil { go view.parent.cmdProcessor.HandleCommand(cmd) } else { - go view.SendMessage(mautrix.MsgText, text) + go view.SendMessage(event.MsgText, text) } view.editMoveText = "" view.SetInputText("") } -func (view *RoomView) Download(url mautrix.ContentURI, filename string, openFile bool) { +func (view *RoomView) Download(url id.ContentURI, filename string, openFile bool) { path, err := view.parent.matrix.DownloadToDisk(url, filename) if err != nil { view.AddServiceMessage(fmt.Sprintf("Failed to download media: %v", err)) @@ -627,7 +638,7 @@ func (view *RoomView) Download(url mautrix.ContentURI, filename string, openFile } } -func (view *RoomView) Redact(eventID, reason string) { +func (view *RoomView) Redact(eventID id.EventID, reason string) { defer debug.Recover() err := view.parent.matrix.Redact(view.Room.ID, eventID, reason) if err != nil { @@ -642,16 +653,16 @@ func (view *RoomView) Redact(eventID, reason string) { } } -func (view *RoomView) SendReaction(eventID string, reaction string) { +func (view *RoomView) SendReaction(eventID id.EventID, reaction string) { defer debug.Recover() debug.Print("Reacting to", eventID, "in", view.Room.ID, "with", reaction) - eventID, err := view.parent.matrix.SendEvent(&event.Event{ - Event: &mautrix.Event{ - Type: mautrix.EventReaction, + eventID, err := view.parent.matrix.SendEvent(&muksevt.Event{ + Event: &event.Event{ + Type: event.EventReaction, RoomID: view.Room.ID, - Content: mautrix.Content{ - RelatesTo: &mautrix.RelatesTo{ - Type: mautrix.RelAnnotation, + Content: event.Content{ + RelatesTo: &event.RelatesTo{ + Type: event.RelAnnotation, EventID: eventID, Key: reaction, }, @@ -670,11 +681,11 @@ func (view *RoomView) SendReaction(eventID string, reaction string) { } } -func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { +func (view *RoomView) SendMessage(msgtype event.MessageType, text string) { view.SendMessageHTML(msgtype, text, "") } -func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html string) { +func (view *RoomView) SendMessageHTML(msgtype event.MessageType, text, html string) { defer debug.Recover() debug.Print("Sending message", msgtype, text, "to", view.Room.ID) if !view.config.Preferences.DisableEmojis { @@ -683,12 +694,12 @@ func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html st var rel *ifc.Relation if view.editing != nil { rel = &ifc.Relation{ - Type: mautrix.RelReplace, + Type: event.RelReplace, Event: view.editing, } } else if view.replying != nil { rel = &ifc.Relation{ - Type: mautrix.RelReference, + Type: event.RelReference, Event: view.replying, } } @@ -699,7 +710,7 @@ func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html st view.status.SetText(view.GetStatus()) eventID, err := view.parent.matrix.SendEvent(evt) if err != nil { - msg.State = event.StateSendFail + msg.State = muksevt.StateSendFail // Show shorter version if available if httpErr, ok := err.(mautrix.HTTPError); ok { err = httpErr @@ -712,7 +723,7 @@ func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html st } else { debug.Print("Event ID received:", eventID) msg.EventID = eventID - msg.State = event.StateDefault + msg.State = muksevt.StateDefault view.MessageView().setMessageID(msg) view.parent.parent.Render() } @@ -734,8 +745,8 @@ func (view *RoomView) Update() { } func (view *RoomView) UpdateUserList() { - pls := &mautrix.PowerLevels{} - if plEvent := view.Room.GetStateEvent(mautrix.StatePowerLevels, ""); plEvent != nil { + pls := &event.PowerLevels{} + if plEvent := view.Room.GetStateEvent(event.StatePowerLevels, ""); plEvent != nil { pls = plEvent.Content.GetPowerLevels() } view.userList.Update(view.Room.GetMembers(), pls) @@ -746,17 +757,17 @@ func (view *RoomView) AddServiceMessage(text string) { view.content.AddMessage(messages.NewServiceMessage(text), AppendMessage) } -func (view *RoomView) parseEvent(evt *event.Event) *messages.UIMessage { +func (view *RoomView) parseEvent(evt *muksevt.Event) *messages.UIMessage { return messages.ParseEvent(view.parent.matrix, view.parent, view.Room, evt) } -func (view *RoomView) AddHistoryEvent(evt *event.Event) { +func (view *RoomView) AddHistoryEvent(evt *muksevt.Event) { if msg := view.parseEvent(evt); msg != nil { view.content.AddMessage(msg, PrependMessage) } } -func (view *RoomView) AddEvent(evt *event.Event) ifc.Message { +func (view *RoomView) AddEvent(evt *muksevt.Event) ifc.Message { if msg := view.parseEvent(evt); msg != nil { view.content.AddMessage(msg, AppendMessage) return msg @@ -764,17 +775,17 @@ func (view *RoomView) AddEvent(evt *event.Event) ifc.Message { return nil } -func (view *RoomView) AddRedaction(redactedEvt *event.Event) { +func (view *RoomView) AddRedaction(redactedEvt *muksevt.Event) { view.AddEvent(redactedEvt) } -func (view *RoomView) AddEdit(evt *event.Event) { +func (view *RoomView) AddEdit(evt *muksevt.Event) { if msg := view.parseEvent(evt); msg != nil { view.content.AddMessage(msg, IgnoreMessage) } } -func (view *RoomView) AddReaction(evt *event.Event, key string) { +func (view *RoomView) AddReaction(evt *muksevt.Event, key string) { msgView := view.MessageView() msg := msgView.getMessageByID(evt.ID) if msg == nil { @@ -790,7 +801,7 @@ func (view *RoomView) AddReaction(evt *event.Event, key string) { } } -func (view *RoomView) GetEvent(eventID string) ifc.Message { +func (view *RoomView) GetEvent(eventID id.EventID) ifc.Message { message, ok := view.content.messageIDs[eventID] if !ok { return nil diff --git a/ui/view-login.go b/ui/view-login.go index 9eb8226..d1cd4ee 100644 --- a/ui/view-login.go +++ b/ui/view-login.go @@ -19,6 +19,7 @@ package ui import ( "math" + "maunium.net/go/mautrix/id" "maunium.net/go/tcell" "maunium.net/go/mautrix" @@ -75,7 +76,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component { hs := ui.gmx.Config().HS view.homeserver.SetPlaceholder("https://example.com").SetText(hs) - view.username.SetPlaceholder("@user:example.com").SetText(ui.gmx.Config().UserID) + view.username.SetPlaceholder("@user:example.com").SetText(string(ui.gmx.Config().UserID)) view.password.SetPlaceholder("correct horse battery staple").SetMaskCharacter('*') view.quitButton.SetOnClick(func() { ui.gmx.Stop(true) }).SetBackgroundColor(tcell.ColorDarkCyan) @@ -103,7 +104,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component { } func (view *LoginView) resolveWellKnown() { - _, homeserver, err := mautrix.ParseUserID(view.username.GetText()) + _, homeserver, err := id.UserID(view.username.GetText()).Parse() if err != nil { return } diff --git a/ui/view-main.go b/ui/view-main.go index 870c98b..581e186 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -27,6 +27,7 @@ import ( sync "github.com/sasha-s/go-deadlock" "maunium.net/go/gomuks/ui/messages" + "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -34,9 +35,9 @@ import ( "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/notification" - "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix/pushrules" ) type MainView struct { @@ -45,7 +46,7 @@ type MainView struct { roomList *RoomList roomView *mauview.Box currentRoom *RoomView - rooms map[string]*RoomView + rooms map[id.RoomID]*RoomView roomsLock sync.RWMutex cmdProcessor *CommandProcessor focused mauview.Focusable @@ -64,7 +65,7 @@ func (ui *GomuksUI) NewMainView() mauview.Component { mainView := &MainView{ flex: mauview.NewFlex().SetDirection(mauview.FlexColumn), roomView: mauview.NewBox(nil).SetBorder(false), - rooms: make(map[string]*RoomView), + rooms: make(map[id.RoomID]*RoomView), matrix: ui.gmx.Matrix(), gmx: ui.gmx, @@ -295,7 +296,7 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView { return nil } -func (view *MainView) GetRoom(roomID string) ifc.RoomView { +func (view *MainView) GetRoom(roomID id.RoomID) ifc.RoomView { room, ok := view.getRoomView(roomID, true) if !ok { return view.addRoom(view.matrix.GetOrCreateRoom(roomID)) @@ -303,7 +304,7 @@ func (view *MainView) GetRoom(roomID string) ifc.RoomView { return room } -func (view *MainView) getRoomView(roomID string, lock bool) (room *RoomView, ok bool) { +func (view *MainView) getRoomView(roomID id.RoomID, lock bool) (room *RoomView, ok bool) { if lock { view.roomsLock.RLock() room, ok = view.rooms[roomID] @@ -357,7 +358,7 @@ func (view *MainView) addRoom(room *rooms.Room) *RoomView { func (view *MainView) SetRooms(rooms *rooms.RoomCache) { view.roomList.Clear() view.roomsLock.Lock() - view.rooms = make(map[string]*RoomView) + view.rooms = make(map[id.RoomID]*RoomView) for _, room := range rooms.Map { if room.HasLeft { continue @@ -383,7 +384,7 @@ func (view *MainView) UpdateTags(room *rooms.Room) { view.parent.Render() } -func (view *MainView) SetTyping(roomID string, users []string) { +func (view *MainView) SetTyping(roomID id.RoomID, users []id.UserID) { roomView, ok := view.getRoomView(roomID, true) if ok { roomView.SetTyping(users) @@ -438,7 +439,7 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul message.SetIsHighlight(should.Highlight) } -func (view *MainView) LoadHistory(roomID string) { +func (view *MainView) LoadHistory(roomID id.RoomID) { defer debug.Recover() roomView, ok := view.getRoomView(roomID, true) if !ok { diff --git a/ui/widget/color.go b/ui/widget/color.go index edb966c..5a8d001 100644 --- a/ui/widget/color.go +++ b/ui/widget/color.go @@ -21,6 +21,8 @@ import ( "hash/fnv" "maunium.net/go/tcell" + + "maunium.net/go/mautrix/id" ) var colorNames = []string{ @@ -201,8 +203,17 @@ func GetHashColorName(s string) string { // GetHashColor gets the tcell Color value for the given string. // // GetHashColor calls GetHashColorName() and gets the Color value from the tcell.ColorNames map. -func GetHashColor(s string) tcell.Color { - return tcell.ColorNames[GetHashColorName(s)] +func GetHashColor(val interface{}) tcell.Color { + switch str := val.(type) { + case string: + return tcell.ColorNames[GetHashColorName(str)] + case *string: + return tcell.ColorNames[GetHashColorName(*str)] + case id.UserID: + return tcell.ColorNames[GetHashColorName(string(str))] + default: + return tcell.ColorNames["red"] + } } // AddColor adds tview color tags to the given string. |