Update stuff and move pushrules to mautrix-go

This commit is contained in:
Tulir Asokan 2020-04-16 19:27:35 +03:00
parent ff20c2c44f
commit 815190be14
47 changed files with 625 additions and 3439 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
// Package pushrules contains utilities to parse push notification rules.
package pushrules

View File

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

View File

@ -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"
}
]
}
}`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = ""

View File

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

View File

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

View File

@ -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())

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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/mautrix"
"maunium.net/go/tcell"
"maunium.net/go/mautrix"
"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

View File

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

View File

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

View File

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