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" "gopkg.in/yaml.v2"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
@ -52,9 +53,9 @@ type UserPreferences struct {
// Config contains the main config of gomuks. // Config contains the main config of gomuks.
type Config struct { type Config struct {
UserID string `yaml:"mxid"` UserID id.UserID `yaml:"mxid"`
AccessToken string `yaml:"access_token"` AccessToken string `yaml:"access_token"`
HS string `yaml:"homeserver"` HS string `yaml:"homeserver"`
RoomCacheSize int `yaml:"room_cache_size"` RoomCacheSize int `yaml:"room_cache_size"`
RoomCacheAge int64 `yaml:"room_cache_age"` 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 return config.UserID
} }
func (config *Config) SaveFilterID(_, filterID string) { func (config *Config) SaveFilterID(_ id.UserID, filterID string) {
config.AuthCache.FilterID = filterID config.AuthCache.FilterID = filterID
config.SaveAuthCache() config.SaveAuthCache()
} }
func (config *Config) LoadFilterID(_ string) string { func (config *Config) LoadFilterID(_ id.UserID) string {
return config.AuthCache.FilterID return config.AuthCache.FilterID
} }
func (config *Config) SaveNextBatch(_, nextBatch string) { func (config *Config) SaveNextBatch(_ id.UserID, nextBatch string) {
config.AuthCache.NextBatch = nextBatch config.AuthCache.NextBatch = nextBatch
config.SaveAuthCache() config.SaveAuthCache()
} }
func (config *Config) LoadNextBatch(_ string) string { func (config *Config) LoadNextBatch(_ id.UserID) string {
return config.AuthCache.NextBatch return config.AuthCache.NextBatch
} }
func (config *Config) SaveRoom(room *mautrix.Room) { func (config *Config) SaveRoom(_ *mautrix.Room) {
panic("SaveRoom is not supported") 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") 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) 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 package ifc
import ( import (
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/matrix/event"
"maunium.net/go/mautrix" "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" "maunium.net/go/gomuks/matrix/rooms"
) )
type Relation struct { type Relation struct {
Type mautrix.RelationType Type event.RelationType
Event *event.Event Event *muksevt.Event
} }
type MatrixContainer interface { type MatrixContainer interface {
@ -42,23 +44,23 @@ type MatrixContainer interface {
Logout() Logout()
SendPreferencesToMatrix() SendPreferencesToMatrix()
PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, relation *Relation) *event.Event PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, relation *Relation) *muksevt.Event
SendEvent(evt *event.Event) (string, error) SendEvent(evt *muksevt.Event) (id.EventID, error)
Redact(roomID, eventID, reason string) error Redact(roomID id.RoomID, eventID id.EventID, reason string) error
SendTyping(roomID string, typing bool) SendTyping(roomID id.RoomID, typing bool)
MarkRead(roomID, eventID string) MarkRead(roomID id.RoomID, eventID id.EventID)
JoinRoom(roomID, server string) (*rooms.Room, error) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error)
LeaveRoom(roomID string) error LeaveRoom(roomID id.RoomID) error
CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error)
FetchMembers(room *rooms.Room) error FetchMembers(room *rooms.Room) error
GetHistory(room *rooms.Room, limit int) ([]*event.Event, error) GetHistory(room *rooms.Room, limit int) ([]*muksevt.Event, error)
GetEvent(room *rooms.Room, eventID string) (*event.Event, error) GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error)
GetRoom(roomID string) *rooms.Room GetRoom(roomID id.RoomID) *rooms.Room
GetOrCreateRoom(roomID string) *rooms.Room GetOrCreateRoom(roomID id.RoomID) *rooms.Room
Download(uri mautrix.ContentURI) ([]byte, error) Download(uri id.ContentURI) ([]byte, error)
DownloadToDisk(uri mautrix.ContentURI, target string) (string, error) DownloadToDisk(uri id.ContentURI, target string) (string, error)
GetDownloadURL(uri mautrix.ContentURI) string GetDownloadURL(uri id.ContentURI) string
GetCachePath(uri mautrix.ContentURI) string GetCachePath(uri id.ContentURI) string
} }

View File

@ -19,9 +19,10 @@ package ifc
import ( import (
"time" "time"
"maunium.net/go/gomuks/matrix/event" "maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
) )
type UIProvider func(gmx Gomuks) GomuksUI type UIProvider func(gmx Gomuks) GomuksUI
@ -40,7 +41,7 @@ type GomuksUI interface {
} }
type MainView interface { type MainView interface {
GetRoom(roomID string) RoomView GetRoom(roomID id.RoomID) RoomView
AddRoom(room *rooms.Room) AddRoom(room *rooms.Room)
RemoveRoom(room *rooms.Room) RemoveRoom(room *rooms.Room)
SetRooms(rooms *rooms.RoomCache) SetRooms(rooms *rooms.RoomCache)
@ -48,7 +49,7 @@ type MainView interface {
UpdateTags(room *rooms.Room) 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) NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
} }
@ -57,23 +58,23 @@ type RoomView interface {
MxRoom() *rooms.Room MxRoom() *rooms.Room
SetCompletions(completions []string) SetCompletions(completions []string)
SetTyping(users []string) SetTyping(users []id.UserID)
UpdateUserList() UpdateUserList()
AddEvent(evt *event.Event) Message AddEvent(evt *muksevt.Event) Message
AddRedaction(evt *event.Event) AddRedaction(evt *muksevt.Event)
AddEdit(evt *event.Event) AddEdit(evt *muksevt.Event)
AddReaction(evt *event.Event, key string) AddReaction(evt *muksevt.Event, key string)
GetEvent(eventID string) Message GetEvent(eventID id.EventID) Message
AddServiceMessage(message string) AddServiceMessage(message string)
} }
type Message interface { type Message interface {
ID() string ID() id.EventID
Time() time.Time Time() time.Time
NotificationSenderName() string NotificationSenderName() string
NotificationContent() string NotificationContent() string
SetIsHighlight(highlight bool) 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" sync "github.com/sasha-s/go-deadlock"
bolt "go.etcd.io/bbolt" 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/gomuks/matrix/rooms"
"maunium.net/go/mautrix" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
) )
type HistoryManager struct { type HistoryManager struct {
@ -87,7 +88,7 @@ func (hm *HistoryManager) Close() error {
var ( var (
EventNotFoundError = errors.New("event not found") 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) { 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 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) eventData := stream.Get(index)
if eventData == nil || len(eventData) == 0 { if eventData == nil || len(eventData) == 0 {
return nil, EventNotFoundError return nil, EventNotFoundError
@ -111,7 +112,7 @@ func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byt
return unmarshalEvent(eventData) 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 { err = hm.db.View(func(tx *bolt.Tx) error {
if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil { if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil {
return err return err
@ -123,7 +124,7 @@ func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Even
return 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 { return hm.db.Update(func(tx *bolt.Tx) error {
if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil { if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil {
return err 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) 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) 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() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
newEvents := make([]*event.Event, len(events)) newEvents := make([]*muksevt.Event, len(events))
err := hm.db.Update(func(tx *bolt.Tx) error { err := hm.db.Update(func(tx *bolt.Tx) error {
streamPointers := tx.Bucket(bucketStreamPointers) streamPointers := tx.Bucket(bucketStreamPointers)
rid := []byte(room.ID) rid := []byte(room.ID)
@ -177,7 +178,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen
return err return err
} }
for i, evt := range 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 { if err := put(stream, eventIDs, newEvents[i], ptrStart+uint64(i)); err != nil {
return err return err
} }
@ -198,7 +199,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen
} }
eventCount := uint64(len(events)) eventCount := uint64(len(events))
for i, evt := range 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 { if err := put(stream, eventIDs, newEvents[i], -ptrStart-uint64(i)); err != nil {
return err return err
} }
@ -215,12 +216,11 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen
return newEvents, err 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() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
err = hm.db.View(func(tx *bolt.Tx) error { err = hm.db.View(func(tx *bolt.Tx) error {
rid := []byte(room.ID) stream := tx.Bucket(bucketRoomStreams).Bucket([]byte(room.ID))
stream := tx.Bucket(bucketRoomStreams).Bucket(rid)
if stream == nil { if stream == nil {
return nil return nil
} }
@ -265,7 +265,7 @@ func btoi(b []byte) uint64 {
return binary.BigEndian.Uint64(b) return binary.BigEndian.Uint64(b)
} }
func marshalEvent(evt *event.Event) ([]byte, error) { func marshalEvent(evt *muksevt.Event) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
enc := gzip.NewWriter(&buf) enc := gzip.NewWriter(&buf)
if err := gob.NewEncoder(enc).Encode(evt); err != nil { if err := gob.NewEncoder(enc).Encode(evt); err != nil {
@ -277,8 +277,8 @@ func marshalEvent(evt *event.Event) ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
func unmarshalEvent(data []byte) (*event.Event, error) { func unmarshalEvent(data []byte) (*muksevt.Event, error) {
evt := &event.Event{} evt := &muksevt.Event{}
if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil { if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil {
return nil, err return nil, err
} else if err := gob.NewDecoder(cmpReader).Decode(evt); err != nil { } else if err := gob.NewDecoder(cmpReader).Decode(evt); err != nil {
@ -290,7 +290,7 @@ func unmarshalEvent(data []byte) (*event.Event, error) {
return evt, nil 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) data, err := marshalEvent(evt)
if err != nil { if err != nil {
return err return err

View File

@ -36,15 +36,17 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"maunium.net/go/gomuks/lib/open" "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"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix/pushrules"
) )
// Container is a wrapper for a mautrix Client and some other stuff. // Container is a wrapper for a mautrix Client and some other stuff.
@ -96,7 +98,8 @@ func (c *Container) InitClient() error {
c.client = nil c.client = nil
} }
var mxid, accessToken string var mxid id.UserID
var accessToken string
if len(c.config.AccessToken) > 0 { if len(c.config.AccessToken) > 0 {
accessToken = c.config.AccessToken accessToken = c.config.AccessToken
mxid = c.config.UserID mxid = c.config.UserID
@ -180,7 +183,7 @@ func respondHTML(w http.ResponseWriter, status int, message string) {
} }
func (c *Container) SingleSignOn() error { 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", "redirectUrl": "http://localhost:29325",
}) })
err := open.Open(loginURL) 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. // UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object.
func (c *Container) UpdatePushRules() { func (c *Container) UpdatePushRules() {
debug.Print("Updating push rules...") debug.Print("Updating push rules...")
resp, err := pushrules.GetPushRules(c.client) resp, err := c.client.GetPushRules()
if err != nil { if err != nil {
debug.Print("Failed to fetch push rules:", err) debug.Print("Failed to fetch push rules:", err)
c.config.PushRules = &pushrules.PushRuleset{} c.config.PushRules = &pushrules.PushRuleset{}
@ -285,7 +288,10 @@ func (c *Container) PushRules() *pushrules.PushRuleset {
return c.config.PushRules 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. // OnLogin initializes the syncer and updates the room list.
func (c *Container) OnLogin() { func (c *Container) OnLogin() {
@ -295,21 +301,21 @@ func (c *Container) OnLogin() {
debug.Print("Initializing syncer") debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config) c.syncer = NewGomuksSyncer(c.config)
c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) c.syncer.OnEventType(event.EventMessage, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage) c.syncer.OnEventType(event.EventEncrypted, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage) c.syncer.OnEventType(event.EventSticker, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventReaction, c.HandleMessage) c.syncer.OnEventType(event.EventReaction, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction) c.syncer.OnEventType(event.EventRedaction, c.HandleRedaction)
c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage) c.syncer.OnEventType(event.StateAliases, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage) c.syncer.OnEventType(event.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage) c.syncer.OnEventType(event.StateTopic, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateRoomName, c.HandleMessage) c.syncer.OnEventType(event.StateRoomName, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership) c.syncer.OnEventType(event.StateMember, c.HandleMembership)
c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt) c.syncer.OnEventType(event.EphemeralEventReceipt, c.HandleReadReceipt)
c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping) c.syncer.OnEventType(event.EphemeralEventTyping, c.HandleTyping)
c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo) c.syncer.OnEventType(event.AccountDataDirectChats, c.HandleDirectChatInfo)
c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules) c.syncer.OnEventType(event.AccountDataPushRules, c.HandlePushRules)
c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag) c.syncer.OnEventType(event.AccountDataRoomTags, c.HandleTag)
c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences) c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences)
c.syncer.InitDoneCallback = func() { c.syncer.InitDoneCallback = func() {
debug.Print("Initial sync done") 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 { if source&EventSourceAccountData == 0 {
return return
} }
@ -395,18 +401,17 @@ func (c *Container) Preferences() *config.UserPreferences {
func (c *Container) SendPreferencesToMatrix() { func (c *Container) SendPreferencesToMatrix() {
defer debug.Recover() defer debug.Recover()
debug.Print("Sending updated preferences:", c.config.Preferences) 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) _, err := c.client.MakeRequest("PUT", u, &c.config.Preferences, nil)
if err != nil { if err != nil {
debug.Print("Failed to update preferences:", err) 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) room := c.GetOrCreateRoom(evt.RoomID)
var redactedEvt *event.Event var redactedEvt *muksevt.Event
err := c.history.Update(room, evt.Redacts, func(redacted *event.Event) error { err := c.history.Update(room, evt.Redacts, func(redacted *muksevt.Event) error {
redacted.Unsigned.RedactedBy = evt.ID
redacted.Unsigned.RedactedBecause = evt redacted.Unsigned.RedactedBecause = evt
redactedEvt = redacted redactedEvt = redacted
return nil 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) { func (c *Container) HandleEdit(room *rooms.Room, editsID id.EventID, editEvent *muksevt.Event) {
var origEvt *event.Event var origEvt *muksevt.Event
err := c.history.Update(room, editsID, func(evt *event.Event) error { err := c.history.Update(room, editsID, func(evt *muksevt.Event) error {
evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent) evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent)
origEvt = evt origEvt = evt
return nil 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() rel := reactEvent.Content.GetRelatesTo()
var origEvt *event.Event var origEvt *muksevt.Event
err := c.history.Update(room, reactsTo, func(evt *event.Event) error { err := c.history.Update(room, reactsTo, func(evt *muksevt.Event) error {
if evt.Unsigned.Relations.Annotations.Map == nil { if evt.Unsigned.Relations.Annotations.Map == nil {
evt.Unsigned.Relations.Annotations.Map = make(map[string]int) 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. // 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) room := c.GetOrCreateRoom(mxEvent.RoomID)
if source&EventSourceLeave != 0 { if source&EventSourceLeave != 0 {
room.HasLeft = true 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 { if editID := mxEvent.Content.GetRelatesTo().GetReplaceID(); len(editID) > 0 {
c.HandleEdit(room, editID, event.Wrap(mxEvent)) c.HandleEdit(room, editID, muksevt.Wrap(mxEvent))
return return
} else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == mautrix.EventReaction && len(reactionID) > 0 { } else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == event.EventReaction && len(reactionID) > 0 {
c.HandleReaction(room, reactionID, event.Wrap(mxEvent)) c.HandleReaction(room, reactionID, muksevt.Wrap(mxEvent))
return return
} }
events, err := c.history.Append(room, []*mautrix.Event{mxEvent}) events, err := c.history.Append(room, []*event.Event{mxEvent})
if err != nil { if err != nil {
debug.Printf("Failed to add event %s to history: %v", mxEvent.ID, err) 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. // 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 isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0 isTimeline := source&EventSourceTimeline != 0
if isLeave { if isLeave {
@ -558,7 +563,7 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
isNonTimelineLeave := isLeave && !isTimeline isNonTimelineLeave := isLeave && !isTimeline
if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave {
return 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) c.processOwnMembershipChange(evt)
} else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) { } 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. // 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) c.HandleMessage(source, evt)
} }
func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { func (c *Container) processOwnMembershipChange(evt *event.Event) {
membership := evt.Content.Membership membership := evt.Content.Membership
prevMembership := mautrix.MembershipLeave prevMembership := event.MembershipLeave
if evt.Unsigned.PrevContent != nil { if evt.Unsigned.PrevContent != nil {
prevMembership = evt.Unsigned.PrevContent.Membership prevMembership = evt.Unsigned.PrevContent.Membership
} }
@ -603,7 +608,7 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
c.ui.Render() 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 var largestTimestamp int64
for eventID, rawContent := range evt.Content.Raw { for eventID, rawContent := range evt.Content.Raw {
content, ok := rawContent.(map[string]interface{}) content, ok := rawContent.(map[string]interface{})
@ -616,7 +621,7 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent
continue continue
} }
myInfo, ok := mRead[c.config.UserID].(map[string]interface{}) myInfo, ok := mRead[string(c.config.UserID)].(map[string]interface{})
if !ok { if !ok {
continue continue
} }
@ -624,13 +629,13 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent
ts, ok := myInfo["ts"].(float64) ts, ok := myInfo["ts"].(float64)
if int64(ts) > largestTimestamp { if int64(ts) > largestTimestamp {
largestTimestamp = int64(ts) largestTimestamp = int64(ts)
largestTimestampEvent = eventID largestTimestampEvent = id.EventID(eventID)
} }
} }
return return
} }
func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { func (c *Container) HandleReadReceipt(source EventSource, evt *event.Event) {
if source&EventSourceLeave != 0 { if source&EventSourceLeave != 0 {
return 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) directChats := make(map[*rooms.Room]bool)
for _, rawRoomIDList := range evt.Content.Raw { for _, rawRoomIDList := range evt.Content.Raw {
roomIDList, ok := rawRoomIDList.([]interface{}) roomIDList, ok := rawRoomIDList.([]interface{})
@ -663,7 +668,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool
continue continue
} }
room := c.GetOrCreateRoom(roomID) room := c.GetOrCreateRoom(id.RoomID(roomID))
if room != nil && !room.HasLeft { if room != nil && !room.HasLeft {
directChats[room] = true directChats[room] = true
} }
@ -672,7 +677,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool
return directChats return directChats
} }
func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) { func (c *Container) HandleDirectChatInfo(_ EventSource, evt *event.Event) {
directChats := c.parseDirectChatInfo(evt) directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Rooms.Map { for _, room := range c.config.Rooms.Map {
shouldBeDirect := directChats[room] 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. // 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") debug.Print("Received updated push rules")
var err error var err error
c.config.PushRules, err = pushrules.EventToPushRules(evt) 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. // 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)) debug.Printf("Received tags for %s: %s -- %s", evt.RoomID, evt.Content.RoomTags, string(evt.Content.VeryRaw))
room := c.GetOrCreateRoom(evt.RoomID) 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. // 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 { if !c.config.AuthCache.InitialSyncDone {
return return
} }
c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs) 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) urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID)
_, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil) _, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil)
} }
func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, rel *ifc.Relation) *event.Event { func (c *Container) PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, rel *ifc.Relation) *muksevt.Event {
var content mautrix.Content var content event.Content
if html != "" { if html != "" {
content = mautrix.Content{ content = event.Content{
FormattedBody: html, FormattedBody: html,
Format: mautrix.FormatHTML, Format: event.FormatHTML,
Body: text, Body: text,
MsgType: msgtype, MsgType: msgtype,
} }
@ -750,49 +755,49 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag
content.MsgType = msgtype content.MsgType = msgtype
} }
if rel != nil && rel.Type == mautrix.RelReplace { if rel != nil && rel.Type == event.RelReplace {
contentCopy := content contentCopy := content
content.NewContent = &contentCopy content.NewContent = &contentCopy
content.Body = "* " + content.Body content.Body = "* " + content.Body
if len(content.FormattedBody) > 0 { if len(content.FormattedBody) > 0 {
content.FormattedBody = "* " + content.FormattedBody content.FormattedBody = "* " + content.FormattedBody
} }
content.RelatesTo = &mautrix.RelatesTo{ content.RelatesTo = &event.RelatesTo{
Type: mautrix.RelReplace, Type: event.RelReplace,
EventID: rel.Event.ID, 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) content.SetReply(rel.Event.Event)
} }
txnID := c.client.TxnID() txnID := c.client.TxnID()
localEcho := event.Wrap(&mautrix.Event{ localEcho := muksevt.Wrap(&event.Event{
ID: txnID, ID: id.EventID(txnID),
Sender: c.config.UserID, Sender: c.config.UserID,
Type: mautrix.EventMessage, Type: event.EventMessage,
Timestamp: time.Now().UnixNano() / 1e6, Timestamp: time.Now().UnixNano() / 1e6,
RoomID: roomID, RoomID: roomID,
Content: content, Content: content,
Unsigned: mautrix.Unsigned{ Unsigned: event.Unsigned{
TransactionID: txnID, TransactionID: txnID,
}, },
}) })
localEcho.Gomuks.OutgoingState = event.StateLocalEcho localEcho.Gomuks.OutgoingState = muksevt.StateLocalEcho
if rel != nil && rel.Type == mautrix.RelReplace { if rel != nil && rel.Type == event.RelReplace {
localEcho.ID = rel.Event.ID localEcho.ID = rel.Event.ID
localEcho.Gomuks.Edits = []*event.Event{localEcho} localEcho.Gomuks.Edits = []*muksevt.Event{localEcho}
} }
return 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() defer debug.Recover()
_, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason}) _, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason})
return err return err
} }
// SendMessage sends the given event. // 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() defer debug.Recover()
c.client.UserTyping(event.RoomID, false, 0) c.client.UserTyping(event.RoomID, false, 0)
@ -804,13 +809,13 @@ func (c *Container) SendEvent(event *event.Event) (string, error) {
return resp.EventID, nil 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() defer debug.Recover()
_, _ = c.client.UserTyping(roomID, typing, timeout) _, _ = c.client.UserTyping(roomID, typing, timeout)
} }
// SendTyping sets whether or not the user is typing in the given room. // 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() ts := time.Now().Unix()
if (c.typing > ts && typing) || (c.typing == 0 && !typing) { if (c.typing > ts && typing) || (c.typing == 0 && !typing) {
return 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. // JoinRoom makes the current user try to join the given room.
func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) { func (c *Container) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) {
resp, err := c.client.JoinRoom(roomID, server, nil) resp, err := c.client.JoinRoom(string(roomID), server, nil)
if err != nil { if err != nil {
return nil, err 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. // 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) _, err := c.client.LeaveRoom(roomID)
if err != nil { if err != nil {
return err return err
@ -873,7 +878,7 @@ func (c *Container) FetchMembers(room *rooms.Room) error {
} }
// GetHistory fetches room history. // 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) events, err := c.history.Load(room, limit)
if err != nil { if err != nil {
return nil, err return nil, err
@ -893,7 +898,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err
room.PrevBatch = resp.End room.PrevBatch = resp.End
c.config.Rooms.Put(room) c.config.Rooms.Put(room)
if len(resp.Chunk) == 0 { if len(resp.Chunk) == 0 {
return []*event.Event{}, nil return []*muksevt.Event{}, nil
} }
events, err = c.history.Prepend(room, resp.Chunk) events, err = c.history.Prepend(room, resp.Chunk)
if err != nil { if err != nil {
@ -902,7 +907,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err
return events, nil 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) evt, err := c.history.Get(room, eventID)
if err != nil && err != EventNotFoundError { if err != nil && err != EventNotFoundError {
debug.Printf("Failed to get event %s from local cache: %v", eventID, err) 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 { if err != nil {
return nil, err return nil, err
} }
evt = event.Wrap(mxEvent) evt = muksevt.Wrap(mxEvent)
debug.Printf("Loaded event %s from server", eventID) debug.Printf("Loaded event %s from server", eventID)
return evt, nil return evt, nil
} }
// GetOrCreateRoom gets the room instance stored in the session. // 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) return c.config.Rooms.GetOrCreate(roomID)
} }
// GetRoom gets the room instance stored in the session. // 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) return c.config.Rooms.Get(roomID)
} }
@ -949,7 +954,7 @@ func cp(src, dst string) error {
return out.Close() 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) cachePath := c.GetCachePath(uri)
if target == "" { if target == "" {
fullPath = cachePath 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. // 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. // 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) cacheFile := c.GetCachePath(uri)
var info os.FileInfo var info os.FileInfo
if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() { 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 return
} }
func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string { func (c *Container) GetDownloadURL(uri id.ContentURI) string {
dlURL, _ := url.Parse(c.client.HomeserverURL.String()) dlURL, _ := url.Parse(c.client.HomeserverURL.String())
if dlURL.Scheme == "" { if dlURL.Scheme == "" {
dlURL.Scheme = "https" dlURL.Scheme = "https"
@ -1017,7 +1022,7 @@ func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string {
return dlURL.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 var resp *http.Response
resp, err = c.client.Client.Get(c.GetDownloadURL(uri)) resp, err = c.client.Client.Get(c.GetDownloadURL(uri))
if err != nil { 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. // 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. // 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) dir := filepath.Join(c.config.MediaDir, uri.Homeserver)
err := os.MkdirAll(dir, 0700) 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 // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
package event package muksevt
import ( import (
"maunium.net/go/mautrix" "maunium.net/go/mautrix/event"
) )
type Event struct { type Event struct {
*mautrix.Event *event.Event
Gomuks GomuksContent `json:"-"` 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} 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" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
) )
@ -54,25 +56,27 @@ type RoomTag struct {
} }
type UnreadMessage struct { type UnreadMessage struct {
EventID string EventID id.EventID
Counted bool Counted bool
Highlight bool Highlight bool
} }
type Member struct { type Member struct {
mautrix.Member event.Member
// The user who sent the membership event // The user who sent the membership event
Sender string `json:"-"` Sender id.UserID `json:"-"`
} }
// Room represents a single Matrix room. // Room represents a single Matrix room.
type Room struct { type Room struct {
// The room ID. // The room ID.
ID string ID id.RoomID
// Whether or not the user has left the room. // Whether or not the user has left the room.
HasLeft bool HasLeft bool
// Whether or not the room is encrypted.
Encrypted bool
// The first batch of events that has been fetched for this room. // The first batch of events that has been fetched for this room.
// Used for fetching additional history. // 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. // The last_batch field from the most recent sync. Used for fetching member lists.
LastPrevBatch string LastPrevBatch string
// The MXID of the user whose session this room was created for. // The MXID of the user whose session this room was created for.
SessionUserID string SessionUserID id.UserID
SessionMember *Member SessionMember *Member
// The number of unread messages that were notified about. // The number of unread messages that were notified about.
UnreadMessages []UnreadMessage UnreadMessages []UnreadMessage
unreadCountCache *int unreadCountCache *int
highlightCache *bool highlightCache *bool
lastMarkedRead string lastMarkedRead id.EventID
// Whether or not this room is marked as a direct chat. // Whether or not this room is marked as a direct chat.
IsDirect bool IsDirect bool
@ -101,10 +105,10 @@ type Room struct {
// Whether or not the members for this room have been fetched from the server. // Whether or not the members for this room have been fetched from the server.
MembersFetched bool MembersFetched bool
// Room state cache. // 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. // MXID -> Member cache calculated from membership events.
memberCache map[string]*Member memberCache map[id.UserID]*Member
exMemberCache map[string]*Member exMemberCache map[id.UserID]*Member
// The first two non-SessionUserID members in the room. Calculated at // The first two non-SessionUserID members in the room. Calculated at
// the same time as memberCache. // the same time as memberCache.
firstMemberCache *Member firstMemberCache *Member
@ -117,11 +121,11 @@ type Room struct {
// The topic of the room. Directly fetched from the m.room.topic state event. // The topic of the room. Directly fetched from the m.room.topic state event.
topicCache string topicCache string
// The canonical alias of the room. Directly fetched from the m.room.canonical_alias state event. // 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. // Whether or not the room has been tombstoned.
replacedCache bool replacedCache bool
// The room ID that replaced this room. // The room ID that replaced this room.
replacedByCache *string replacedByCache *id.RoomID
// Path for state store file. // Path for state store file.
path string path string
@ -174,7 +178,7 @@ func (room *Room) load() {
return return
} }
debug.Print("Loading state for room", room.ID, "from disk") 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) file, err := os.OpenFile(room.path, os.O_RDONLY, 0600)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@ -265,7 +269,7 @@ func (room *Room) Save() {
} }
// MarkRead clears the new message statuses on this room. // 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() room.lock.Lock()
defer room.lock.Unlock() defer room.lock.Unlock()
if room.lastMarkedRead == eventID { if room.lastMarkedRead == eventID {
@ -319,7 +323,7 @@ func (room *Room) HasNewMessages() bool {
return len(room.UnreadMessages) > 0 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() room.lock.Lock()
defer room.lock.Unlock() defer room.lock.Unlock()
room.UnreadMessages = append(room.UnreadMessages, UnreadMessage{ 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 { func (room *Room) Tags() []RoomTag {
room.lock.RLock() room.lock.RLock()
defer room.lock.RUnlock() defer room.lock.RUnlock()
if len(room.RawTags) == 0 { if len(room.RawTags) == 0 {
if room.IsDirect { if room.IsDirect {
return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}} return []RoomTag{tagDirect}
} else if room.SessionMember != nil && room.SessionMember.Membership == mautrix.MembershipInvite { } else if room.SessionMember != nil && room.SessionMember.Membership == event.MembershipInvite {
return []RoomTag{{"net.maunium.gomuks.fake.invite", "0.5"}} return []RoomTag{tagInvite}
} else if room.SessionMember != nil && room.SessionMember.Membership != mautrix.MembershipJoin { } else if room.SessionMember != nil && room.SessionMember.Membership != event.MembershipJoin {
return []RoomTag{{"net.maunium.gomuks.fake.leave", "0.5"}} return []RoomTag{tagLeave}
} }
return []RoomTag{{"", "0.5"}} return []RoomTag{tagDefault}
} }
return room.RawTags 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 // UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination. // on the type/state_key combination.
func (room *Room) UpdateState(event *mautrix.Event) { func (room *Room) UpdateState(evt *event.Event) {
if event.StateKey == nil { if evt.StateKey == nil {
panic("Tried to UpdateState() event with no state key.") panic("Tried to UpdateState() event with no state key.")
} }
room.Load() room.Load()
room.lock.Lock() room.lock.Lock()
defer room.lock.Unlock() defer room.lock.Unlock()
room.changed = true room.changed = true
_, exists := room.state[event.Type] _, exists := room.state[evt.Type]
if !exists { if !exists {
room.state[event.Type] = make(map[string]*mautrix.Event) room.state[evt.Type] = make(map[string]*event.Event)
} }
switch event.Type { switch evt.Type {
case mautrix.StateRoomName: case event.StateRoomName:
room.NameCache = event.Content.Name room.NameCache = evt.Content.Name
room.nameCacheSource = ExplicitRoomName room.nameCacheSource = ExplicitRoomName
case mautrix.StateCanonicalAlias: case event.StateCanonicalAlias:
if room.nameCacheSource <= CanonicalAliasRoomName { if room.nameCacheSource <= CanonicalAliasRoomName {
room.NameCache = event.Content.Alias room.NameCache = string(evt.Content.Alias)
room.nameCacheSource = CanonicalAliasRoomName room.nameCacheSource = CanonicalAliasRoomName
} }
room.CanonicalAliasCache = event.Content.Alias room.CanonicalAliasCache = evt.Content.Alias
case mautrix.StateMember: case event.StateMember:
if room.nameCacheSource <= MemberRoomName { if room.nameCacheSource <= MemberRoomName {
room.NameCache = "" room.NameCache = ""
} }
room.updateMemberState(event) room.updateMemberState(evt)
case mautrix.StateTopic: case event.StateTopic:
room.topicCache = event.Content.Topic room.topicCache = evt.Content.Topic
} }
if event.Type != mautrix.StateMember { if evt.Type != event.StateMember {
debug.Printf("Updating state %s#%s for %s", event.Type.String(), event.GetStateKey(), room.ID) 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) { func (room *Room) updateMemberState(event *event.Event) {
userID := event.GetStateKey() userID := id.UserID(event.GetStateKey())
if userID == room.SessionUserID { if userID == room.SessionUserID {
debug.Print("Updating session user state:", string(event.Content.VeryRaw)) debug.Print("Updating session user state:", string(event.Content.VeryRaw))
room.SessionMember = room.eventToMember(userID, event.Sender, &event.Content) 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. // 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.Load()
room.lock.RLock() room.lock.RLock()
defer room.lock.RUnlock() 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. // 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] stateEventMap, _ := room.state[eventType]
return stateEventMap return stateEventMap
} }
@ -460,7 +471,7 @@ func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautri
// GetTopic returns the topic of the room. // GetTopic returns the topic of the room.
func (room *Room) GetTopic() string { func (room *Room) GetTopic() string {
if len(room.topicCache) == 0 { if len(room.topicCache) == 0 {
topicEvt := room.GetStateEvent(mautrix.StateTopic, "") topicEvt := room.GetStateEvent(event.StateTopic, "")
if topicEvt != nil { if topicEvt != nil {
room.topicCache = topicEvt.Content.Topic room.topicCache = topicEvt.Content.Topic
} }
@ -468,9 +479,9 @@ func (room *Room) GetTopic() string {
return room.topicCache return room.topicCache
} }
func (room *Room) GetCanonicalAlias() string { func (room *Room) GetCanonicalAlias() id.RoomAlias {
if len(room.CanonicalAliasCache) == 0 { if len(room.CanonicalAliasCache) == 0 {
canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "") canonicalAliasEvt := room.GetStateEvent(event.StateCanonicalAlias, "")
if canonicalAliasEvt != nil { if canonicalAliasEvt != nil {
room.CanonicalAliasCache = canonicalAliasEvt.Content.Alias room.CanonicalAliasCache = canonicalAliasEvt.Content.Alias
} else { } 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. // updateNameFromNameEvent updates the room display name to be the name set in the name event.
func (room *Room) updateNameFromNameEvent() { func (room *Room) updateNameFromNameEvent() {
nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") nameEvt := room.GetStateEvent(event.StateRoomName, "")
if nameEvt != nil { if nameEvt != nil {
room.NameCache = nameEvt.Content.Name room.NameCache = nameEvt.Content.Name
} }
@ -528,7 +539,7 @@ func (room *Room) updateNameCache() {
room.nameCacheSource = ExplicitRoomName room.nameCacheSource = ExplicitRoomName
} }
if len(room.NameCache) == 0 { if len(room.NameCache) == 0 {
room.NameCache = room.GetCanonicalAlias() room.NameCache = string(room.GetCanonicalAlias())
room.nameCacheSource = CanonicalAliasRoomName room.nameCacheSource = CanonicalAliasRoomName
} }
if len(room.NameCache) == 0 { if len(room.NameCache) == 0 {
@ -548,8 +559,8 @@ func (room *Room) GetTitle() string {
func (room *Room) IsReplaced() bool { func (room *Room) IsReplaced() bool {
if room.replacedByCache == nil { if room.replacedByCache == nil {
evt := room.GetStateEvent(mautrix.StateTombstone, "") evt := room.GetStateEvent(event.StateTombstone, "")
var replacement string var replacement id.RoomID
if evt != nil { if evt != nil {
replacement = evt.Content.ReplacementRoom replacement = evt.Content.ReplacementRoom
} }
@ -559,18 +570,18 @@ func (room *Room) IsReplaced() bool {
return room.replacedCache return room.replacedCache
} }
func (room *Room) ReplacedBy() string { func (room *Room) ReplacedBy() id.RoomID {
if room.replacedByCache == nil { if room.replacedByCache == nil {
room.IsReplaced() room.IsReplaced()
} }
return *room.replacedByCache 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 := content.Member
member.Membership = content.Membership member.Membership = content.Membership
if len(member.Displayname) == 0 { if len(member.Displayname) == 0 {
member.Displayname = userID member.Displayname = string(userID)
} }
return &Member{ return &Member{
Member: 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 userID != room.SessionUserID {
if room.firstMemberCache == nil { if room.firstMemberCache == nil {
room.firstMemberCache = member 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. // 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 { if len(room.memberCache) > 0 {
return room.memberCache return room.memberCache
} }
cache := make(map[string]*Member) cache := make(map[id.UserID]*Member)
exCache := make(map[string]*Member) exCache := make(map[id.UserID]*Member)
room.lock.RLock() room.lock.RLock()
events := room.getStateEvents(mautrix.StateMember) memberEvents := room.getStateEvents(event.StateMember)
room.firstMemberCache = nil room.firstMemberCache = nil
room.secondMemberCache = nil room.secondMemberCache = nil
if events != nil { if memberEvents != nil {
for userID, event := range events { for userIDStr, evt := range memberEvents {
member := room.eventToMember(userID, event.Sender, &event.Content) userID := id.UserID(userIDStr)
member := room.eventToMember(userID, evt.Sender, &evt.Content)
if member.Membership.IsInviteOrJoin() { if member.Membership.IsInviteOrJoin() {
cache[userID] = member cache[userID] = member
room.updateNthMemberCache(userID, member) room.updateNthMemberCache(userID, member)
@ -631,7 +643,7 @@ func (room *Room) createMemberCache() map[string]*Member {
// //
// The members are returned from the cache. // The members are returned from the cache.
// If the cache is empty, it is updated first. // 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.Load()
room.createMemberCache() room.createMemberCache()
return room.memberCache return room.memberCache
@ -639,7 +651,7 @@ func (room *Room) GetMembers() map[string]*Member {
// GetMember returns the member with the given MXID. // GetMember returns the member with the given MXID.
// If the member doesn't exist, nil is returned. // 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 { if userID == room.SessionUserID && room.SessionMember != nil {
return room.SessionMember return room.SessionMember
} }
@ -660,16 +672,27 @@ func (room *Room) GetMember(userID string) *Member {
return nil 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. // GetSessionOwner returns the ID of the user whose session this room was created for.
func (room *Room) GetSessionOwner() string { func (room *Room) GetOwnDisplayname() string {
return room.SessionUserID member := room.GetMember(room.SessionUserID)
if member != nil {
return member.Displayname
}
return ""
} }
// NewRoom creates a new Room with the given ID // 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{ return &Room{
ID: roomID, ID: roomID,
state: make(map[mautrix.EventType]map[string]*mautrix.Event), state: make(map[event.Type]map[string]*event.Event),
path: cache.roomPath(roomID), path: cache.roomPath(roomID),
cache: cache, 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" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/mautrix/id"
) )
// RoomCache contains room state info in a hashmap and linked list. // RoomCache contains room state info in a hashmap and linked list.
@ -37,15 +38,15 @@ type RoomCache struct {
directory string directory string
maxSize int maxSize int
maxAge int64 maxAge int64
getOwner func() string getOwner func() id.UserID
Map map[string]*Room Map map[id.RoomID]*Room
head *Room head *Room
tail *Room tail *Room
size int 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{ return &RoomCache{
listPath: listPath, listPath: listPath,
directory: directory, directory: directory,
@ -53,7 +54,7 @@ func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwne
maxAge: maxAge, maxAge: maxAge,
getOwner: getOwner, getOwner: getOwner,
Map: make(map[string]*Room), Map: make(map[id.RoomID]*Room),
} }
} }
@ -88,7 +89,7 @@ func (cache *RoomCache) LoadList() error {
} }
// Read list // Read list
cache.Map = make(map[string]*Room, size) cache.Map = make(map[id.RoomID]*Room, size)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
room := &Room{} room := &Room{}
err = dec.Decode(room) err = dec.Decode(room)
@ -147,7 +148,7 @@ func (cache *RoomCache) SaveList() error {
return nil return nil
} }
func (cache *RoomCache) Touch(roomID string) { func (cache *RoomCache) Touch(roomID id.RoomID) {
cache.Lock() cache.Lock()
node, ok := cache.Map[roomID] node, ok := cache.Map[roomID]
if !ok || node == nil { if !ok || node == nil {
@ -174,14 +175,14 @@ func (cache *RoomCache) touch(node *Room) {
node.touch = time.Now().Unix() node.touch = time.Now().Unix()
} }
func (cache *RoomCache) Get(roomID string) *Room { func (cache *RoomCache) Get(roomID id.RoomID) *Room {
cache.Lock() cache.Lock()
node := cache.get(roomID) node := cache.get(roomID)
cache.Unlock() cache.Unlock()
return node return node
} }
func (cache *RoomCache) GetOrCreate(roomID string) *Room { func (cache *RoomCache) GetOrCreate(roomID id.RoomID) *Room {
cache.Lock() cache.Lock()
node := cache.get(roomID) node := cache.get(roomID)
if node == nil { if node == nil {
@ -192,7 +193,7 @@ func (cache *RoomCache) GetOrCreate(roomID string) *Room {
return node return node
} }
func (cache *RoomCache) get(roomID string) *Room { func (cache *RoomCache) get(roomID id.RoomID) *Room {
node, ok := cache.Map[roomID] node, ok := cache.Map[roomID]
if ok && node != nil { if ok && node != nil {
return node return node
@ -215,11 +216,11 @@ func (cache *RoomCache) Put(room *Room) {
node.Save() node.Save()
} }
func (cache *RoomCache) roomPath(roomID string) string { func (cache *RoomCache) roomPath(roomID id.RoomID) string {
return filepath.Join(cache.directory, roomID+".gob.gz") 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() cache.Lock()
defer cache.Unlock() defer cache.Unlock()
node, ok := cache.Map[roomID] 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) node := NewRoom(roomID, cache)
cache.Map[node.ID] = node cache.Map[node.ID] = node
return node return node

View File

@ -24,14 +24,16 @@ import (
"time" "time"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
type SyncerSession interface { type SyncerSession interface {
GetRoom(id string) *rooms.Room GetRoom(id id.RoomID) *rooms.Room
GetUserID() string GetUserID() id.UserID
} }
type EventSource int type EventSource int
@ -45,6 +47,7 @@ const (
EventSourceTimeline EventSourceTimeline
EventSourceState EventSourceState
EventSourceEphemeral EventSourceEphemeral
EventSourceToDevice
) )
func (es EventSource) String() string { func (es EventSource) String() string {
@ -83,14 +86,14 @@ func (es EventSource) String() string {
return fmt.Sprintf("unknown (%d)", es) 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 // 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 // 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. // pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information.
type GomuksSyncer struct { type GomuksSyncer struct {
Session SyncerSession Session SyncerSession
listeners map[mautrix.EventType][]EventHandler // event type to listeners array listeners map[event.Type][]EventHandler // event type to listeners array
FirstSyncDone bool FirstSyncDone bool
InitDoneCallback func() InitDoneCallback func()
} }
@ -99,7 +102,7 @@ type GomuksSyncer struct {
func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { func NewGomuksSyncer(session SyncerSession) *GomuksSyncer {
return &GomuksSyncer{ return &GomuksSyncer{
Session: session, Session: session,
listeners: make(map[mautrix.EventType][]EventHandler), listeners: make(map[event.Type][]EventHandler),
FirstSyncDone: false, 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) { func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []json.RawMessage, source EventSource) {
for _, event := range events { for _, evt := range events {
if source == EventSourcePresence { s.processSyncEvent(room, evt, source)
debug.Print(string(event))
}
s.processSyncEvent(room, event, source)
} }
} }
func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, eventJSON json.RawMessage, source EventSource) { func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, eventJSON json.RawMessage, source EventSource) {
event := &mautrix.Event{} evt := &event.Event{}
err := json.Unmarshal(eventJSON, event) err := json.Unmarshal(eventJSON, evt)
if err != nil { if err != nil {
debug.Print("Failed to unmarshal event: %v\n%s", err, string(eventJSON)) debug.Print("Failed to unmarshal event: %v\n%s", err, string(eventJSON))
return 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 { if room != nil {
event.RoomID = room.ID evt.RoomID = room.ID
if source&EventSourceState != 0 || (source&EventSourceTimeline != 0 && event.Type.IsState() && event.StateKey != nil) { if evt.Type.IsState() {
room.UpdateState(event) 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. // OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks. // 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] _, exists := s.listeners[eventType]
if !exists { if !exists {
s.listeners[eventType] = []EventHandler{} 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) s.listeners[eventType] = append(s.listeners[eventType], callback)
} }
func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) { func (s *GomuksSyncer) notifyListeners(source EventSource, evt *event.Event) {
if (event.Type.IsState() && source&EventSourceState == 0 && event.StateKey == nil) || listeners, exists := s.listeners[evt.Type]
(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]
if !exists { if !exists {
return return
} }
for _, fn := range listeners { 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. // 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{ filter := &mautrix.Filter{
Room: mautrix.RoomFilter{ Room: mautrix.RoomFilter{
IncludeLeave: false, IncludeLeave: false,
State: mautrix.FilterPart{ State: mautrix.FilterPart{
LazyLoadMembers: true, LazyLoadMembers: true,
Types: []string{ Types: []event.Type{
"m.room.member", event.StateMember,
"m.room.name", event.StateRoomName,
"m.room.topic", event.StateTopic,
"m.room.canonical_alias", event.StateCanonicalAlias,
"m.room.aliases", event.StatePowerLevels,
"m.room.power_levels", event.StateTombstone,
"m.room.tombstone",
}, },
}, },
Timeline: mautrix.FilterPart{ Timeline: mautrix.FilterPart{
LazyLoadMembers: true, LazyLoadMembers: true,
Types: []string{ Types: []event.Type{
"m.room.message", event.EventMessage,
"m.room.redaction", event.EventRedaction,
"m.room.encrypted", event.EventEncrypted,
"m.sticker", event.EventSticker,
"m.reaction", event.EventReaction,
"m.room.member", event.StateMember,
"m.room.name", event.StateRoomName,
"m.room.topic", event.StateTopic,
"m.room.canonical_alias", event.StateCanonicalAlias,
"m.room.aliases", event.StatePowerLevels,
"m.room.power_levels", event.StateTombstone,
"m.room.tombstone",
}, },
// Limit: 50, Limit: 50,
}, },
Ephemeral: mautrix.FilterPart{ Ephemeral: mautrix.FilterPart{
Types: []string{"m.typing", "m.receipt"}, Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt},
}, },
AccountData: mautrix.FilterPart{ AccountData: mautrix.FilterPart{
Types: []string{"m.tag"}, Types: []event.Type{event.AccountDataRoomTags},
}, },
}, },
AccountData: mautrix.FilterPart{ 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{ Presence: mautrix.FilterPart{
NotTypes: []string{"*"}, NotTypes: []event.Type{event.NewEventType("*")},
}, },
} }
rawFilter, _ := json.Marshal(&filter) 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" "github.com/russross/blackfriday/v2"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
) )
func cmdMe(cmd *Command) { func cmdMe(cmd *Command) {
text := strings.Join(cmd.Args, " ") 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 // 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. // 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, " ") text := strings.Join(cmd.Args, " ")
render := NewRainbowRenderer(blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ render := NewRainbowRenderer(blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
@ -101,15 +103,15 @@ func makeRainbow(cmd *Command, msgtype mautrix.MessageType) {
} }
func cmdRainbow(cmd *Command) { func cmdRainbow(cmd *Command) {
makeRainbow(cmd, mautrix.MsgText) makeRainbow(cmd, event.MsgText)
} }
func cmdRainbowMe(cmd *Command) { func cmdRainbowMe(cmd *Command) {
makeRainbow(cmd, mautrix.MsgEmote) makeRainbow(cmd, event.MsgEmote)
} }
func cmdNotice(cmd *Command) { 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) { 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") cmd.Reply("/accept can only be used in rooms you're invited to")
return return
} }
_, server, _ := mautrix.ParseUserID(room.SessionMember.Sender) _, server, _ := room.SessionMember.Sender.Parse()
_, err := cmd.Matrix.JoinRoom(room.ID, server) _, err := cmd.Matrix.JoinRoom(room.ID, server)
if err != nil { if err != nil {
cmd.Reply("Failed to accept invite:", err) cmd.Reply("Failed to accept invite:", err)
@ -223,11 +225,11 @@ func cmdTag(cmd *Command) {
} }
var err error var err error
if len(cmd.Args) > 2 && cmd.Args[2] == "--reset" { 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))}, cmd.Args[0]: {Order: json.Number(fmt.Sprintf("%f", order))},
} }
for _, tag := range cmd.Room.MxRoom().RawTags { 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) err = cmd.Matrix.Client().SetTags(cmd.Room.MxRoom().ID, tags)
} else { } else {
@ -253,7 +255,7 @@ func cmdRoomNick(cmd *Command) {
room := cmd.Room.MxRoom() room := cmd.Room.MxRoom()
member := room.GetMember(room.SessionUserID) member := room.GetMember(room.SessionUserID)
member.Displayname = strings.Join(cmd.Args, " ") 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 { if err != nil {
cmd.Reply("Failed to set room nick:", err) cmd.Reply("Failed to set room nick:", err)
} }
@ -376,7 +378,7 @@ func cmdInvite(cmd *Command) {
cmd.Reply("Usage: /invite <user id>") cmd.Reply("Usage: /invite <user id>")
return 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 { if err != nil {
debug.Print("Error in invite call:", err) debug.Print("Error in invite call:", err)
cmd.Reply("Failed to invite user: %v", err) cmd.Reply("Failed to invite user: %v", err)
@ -392,7 +394,7 @@ func cmdBan(cmd *Command) {
if len(cmd.Args) >= 2 { if len(cmd.Args) >= 2 {
reason = strings.Join(cmd.Args[1:], " ") 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 { if err != nil {
debug.Print("Error in ban call:", err) debug.Print("Error in ban call:", err)
cmd.Reply("Failed to ban user: %v", err) cmd.Reply("Failed to ban user: %v", err)
@ -405,7 +407,7 @@ func cmdUnban(cmd *Command) {
cmd.Reply("Usage: /unban <user>") cmd.Reply("Usage: /unban <user>")
return 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 { if err != nil {
debug.Print("Error in unban call:", err) debug.Print("Error in unban call:", err)
cmd.Reply("Failed to unban user: %v", err) cmd.Reply("Failed to unban user: %v", err)
@ -421,7 +423,7 @@ func cmdKick(cmd *Command) {
if len(cmd.Args) >= 2 { if len(cmd.Args) >= 2 {
reason = strings.Join(cmd.Args[1:], " ") 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 { if err != nil {
debug.Print("Error in kick call:", err) debug.Print("Error in kick call:", err)
debug.Print("Failed to kick user:", err) debug.Print("Failed to kick user:", err)
@ -445,9 +447,18 @@ func cmdPrivateMessage(cmd *Command) {
if len(cmd.Args) == 0 { if len(cmd.Args) == 0 {
cmd.Reply("Usage: /pm <user id> [more user ids...]") 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{ req := &mautrix.ReqCreateRoom{
Preset: "trusted_private_chat", Preset: "trusted_private_chat",
Invite: cmd.Args, Invite: invites,
} }
room, err := cmd.Matrix.CreateRoom(req) room, err := cmd.Matrix.CreateRoom(req)
if err != nil { if err != nil {
@ -462,7 +473,7 @@ func cmdJoin(cmd *Command) {
cmd.Reply("Usage: /join <room>") cmd.Reply("Usage: /join <room>")
return return
} }
identifer := cmd.Args[0] identifer := id.RoomID(cmd.Args[0])
server := "" server := ""
if len(cmd.Args) > 1 { if len(cmd.Args) > 1 {
server = cmd.Args[1] server = cmd.Args[1]
@ -479,7 +490,7 @@ func cmdMSendEvent(cmd *Command) {
cmd.Reply("Usage: /msend <event type> <content>") cmd.Reply("Usage: /msend <event type> <content>")
return return
} }
cmd.Args = append([]string{cmd.Room.MxRoom().ID}, cmd.Args...) cmd.Args = append([]string{string(cmd.Room.MxRoom().ID)}, cmd.Args...)
cmdSendEvent(cmd) cmdSendEvent(cmd)
} }
@ -488,8 +499,8 @@ func cmdSendEvent(cmd *Command) {
cmd.Reply("Usage: /send <room id> <event type> <content>") cmd.Reply("Usage: /send <room id> <event type> <content>")
return return
} }
roomID := cmd.Args[0] roomID := id.RoomID(cmd.Args[0])
eventType := mautrix.NewEventType(cmd.Args[1]) eventType := event.NewEventType(cmd.Args[1])
rawContent := strings.Join(cmd.Args[2:], " ") rawContent := strings.Join(cmd.Args[2:], " ")
var content interface{} var content interface{}
@ -515,7 +526,7 @@ func cmdMSetState(cmd *Command) {
cmd.Reply("Usage: /msetstate <event type> <state key> <content>") cmd.Reply("Usage: /msetstate <event type> <state key> <content>")
return return
} }
cmd.Args = append([]string{cmd.Room.MxRoom().ID}, cmd.Args...) cmd.Args = append([]string{string(cmd.Room.MxRoom().ID)}, cmd.Args...)
cmdSetState(cmd) cmdSetState(cmd)
} }
@ -525,8 +536,8 @@ func cmdSetState(cmd *Command) {
return return
} }
roomID := cmd.Args[0] roomID := id.RoomID(cmd.Args[0])
eventType := mautrix.NewEventType(cmd.Args[1]) eventType := event.NewEventType(cmd.Args[1])
stateKey := cmd.Args[2] stateKey := cmd.Args[2]
if stateKey == "-" { if stateKey == "-" {
stateKey = "" stateKey = ""

View File

@ -23,6 +23,7 @@ import (
"github.com/lithammer/fuzzysearch/fuzzy" "github.com/lithammer/fuzzysearch/fuzzy"
"maunium.net/go/mautrix/id"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -87,7 +88,7 @@ func (fs *FuzzySearchModal) Blur() {
fs.container.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 { for _, room := range rooms {
if room.Room.IsReplaced() { if room.Room.IsReplaced() {
//if _, ok := rooms[room.Room.ReplacedBy()]; ok //if _, ok := rooms[room.Room.ReplacedBy()]; ok

View File

@ -23,7 +23,8 @@ import (
"github.com/mattn/go-runewidth" "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/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -43,7 +44,7 @@ type memberListItem struct {
rooms.Member rooms.Member
PowerLevel int PowerLevel int
Sigil rune Sigil rune
UserID string UserID id.UserID
Color tcell.Color Color tcell.Color
} }
@ -64,7 +65,7 @@ func (rml roomMemberList) Swap(i, j int) {
rml[i], rml[j] = rml[j], rml[i] 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)) ml.list = make(roomMemberList, len(data))
i := 0 i := 0
highestLevel := math.MinInt32 highestLevel := math.MinInt32

View File

@ -25,10 +25,12 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
sync "github.com/sasha-s/go-deadlock" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/mautrix"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
@ -59,7 +61,7 @@ type MessageView struct {
prevPrefs config.UserPreferences prevPrefs config.UserPreferences
messageIDLock sync.RWMutex messageIDLock sync.RWMutex
messageIDs map[string]*messages.UIMessage messageIDs map[id.EventID]*messages.UIMessage
messagesLock sync.RWMutex messagesLock sync.RWMutex
messages []*messages.UIMessage messages []*messages.UIMessage
msgBufferLock sync.RWMutex msgBufferLock sync.RWMutex
@ -79,7 +81,7 @@ func NewMessageView(parent *RoomView) *MessageView {
ScrollOffset: 0, ScrollOffset: 0,
messages: make([]*messages.UIMessage, 0), messages: make([]*messages.UIMessage, 0),
messageIDs: make(map[string]*messages.UIMessage), messageIDs: make(map[id.EventID]*messages.UIMessage),
msgBuffer: make([]*messages.UIMessage, 0), msgBuffer: make([]*messages.UIMessage, 0),
_width: 80, _width: 80,
@ -95,7 +97,7 @@ func (view *MessageView) Unload() {
view.messagesLock.Lock() view.messagesLock.Lock()
view.msgBufferLock.Lock() view.msgBufferLock.Lock()
view.messageIDLock.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.msgBuffer = make([]*messages.UIMessage, 0)
view.messages = make([]*messages.UIMessage, 0) view.messages = make([]*messages.UIMessage, 0)
view.initialHistoryLoaded = false view.initialHistoryLoaded = false
@ -140,9 +142,9 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil { if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil {
view.replaceMessage(oldMsg, message) view.replaceMessage(oldMsg, message)
direction = IgnoreMessage 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.replaceMessage(oldMsg, message)
view.deleteMessageID(message.TxnID) view.deleteMessageID(id.EventID(message.TxnID))
direction = IgnoreMessage direction = IgnoreMessage
} }
@ -208,7 +210,7 @@ func (view *MessageView) replaceMessage(original *messages.UIMessage, new *messa
view.messagesLock.Unlock() view.messagesLock.Unlock()
} }
func (view *MessageView) getMessageByID(id string) *messages.UIMessage { func (view *MessageView) getMessageByID(id id.EventID) *messages.UIMessage {
if id == "" { if id == "" {
return nil return nil
} }
@ -221,7 +223,7 @@ func (view *MessageView) getMessageByID(id string) *messages.UIMessage {
return msg return msg
} }
func (view *MessageView) deleteMessageID(id string) { func (view *MessageView) deleteMessageID(id id.EventID) {
if id == "" { if id == "" {
return return
} }
@ -365,7 +367,7 @@ func (view *MessageView) handleUsernameClick(message *messages.UIMessage, prevMe
// return false // 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 return false
} }
@ -564,7 +566,7 @@ func (view *MessageView) CapturePlaintext(height int) string {
var sender string var sender string
if len(message.Sender()) > 0 { if len(message.Sender()) > 0 {
sender = fmt.Sprintf(" <%s>", message.Sender()) sender = fmt.Sprintf(" <%s>", message.Sender())
} else if message.Type == mautrix.MsgEmote { } else if message.Type == event.MsgEmote {
sender = fmt.Sprintf(" * %s", message.SenderName) sender = fmt.Sprintf(" * %s", message.SenderName)
} }
fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText()) fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText())

View File

@ -22,8 +22,9 @@ import (
"time" "time"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"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/id"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -64,26 +65,26 @@ func (rs ReactionSlice) Swap(i, j int) {
} }
type UIMessage struct { type UIMessage struct {
EventID string EventID id.EventID
TxnID string TxnID string
Relation mautrix.RelatesTo Relation event.RelatesTo
Type mautrix.MessageType Type event.MessageType
SenderID string SenderID id.UserID
SenderName string SenderName string
DefaultSenderColor tcell.Color DefaultSenderColor tcell.Color
Timestamp time.Time Timestamp time.Time
State event.OutgoingState State muksevt.OutgoingState
IsHighlight bool IsHighlight bool
IsService bool IsService bool
IsSelected bool IsSelected bool
Edited bool Edited bool
Event *event.Event Event *muksevt.Event
ReplyTo *UIMessage ReplyTo *UIMessage
Reactions ReactionSlice Reactions ReactionSlice
Renderer MessageRenderer Renderer MessageRenderer
} }
func (msg *UIMessage) GetEvent() *event.Event { func (msg *UIMessage) GetEvent() *muksevt.Event {
if msg == nil { if msg == nil {
return nil return nil
} }
@ -93,10 +94,10 @@ func (msg *UIMessage) GetEvent() *event.Event {
const DateFormat = "January _2, 2006" const DateFormat = "January _2, 2006"
const TimeFormat = "15:04:05" 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 msgtype := evt.Content.MsgType
if len(msgtype) == 0 { 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)) 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. // In any other case, the sender is the display name of the user who sent the message.
func (msg *UIMessage) Sender() string { func (msg *UIMessage) Sender() string {
switch msg.State { switch msg.State {
case event.StateLocalEcho: case muksevt.StateLocalEcho:
return "Sending..." return "Sending..."
case event.StateSendFail: case muksevt.StateSendFail:
return "Error" return "Error"
} }
switch msg.Type { switch msg.Type {
@ -185,11 +186,11 @@ func (msg *UIMessage) NotificationContent() string {
func (msg *UIMessage) getStateSpecificColor() tcell.Color { func (msg *UIMessage) getStateSpecificColor() tcell.Color {
switch msg.State { switch msg.State {
case event.StateLocalEcho: case muksevt.StateLocalEcho:
return tcell.ColorGray return tcell.ColorGray
case event.StateSendFail: case muksevt.StateSendFail:
return tcell.ColorRed return tcell.ColorRed
case event.StateDefault: case muksevt.StateDefault:
fallthrough fallthrough
default: default:
return tcell.ColorDefault return tcell.ColorDefault
@ -286,14 +287,14 @@ func (msg *UIMessage) SameDate(message *UIMessage) bool {
return day1 == day2 && month1 == month2 && year1 == year2 return day1 == day2 && month1 == month2 && year1 == year2
} }
func (msg *UIMessage) ID() string { func (msg *UIMessage) ID() id.EventID {
if len(msg.EventID) == 0 { if len(msg.EventID) == 0 {
return msg.TxnID return id.EventID(msg.TxnID)
} }
return msg.EventID return msg.EventID
} }
func (msg *UIMessage) SetID(id string) { func (msg *UIMessage) SetID(id id.EventID) {
msg.EventID = id msg.EventID = id
} }

View File

@ -20,7 +20,7 @@ import (
"fmt" "fmt"
"time" "time"
"maunium.net/go/gomuks/matrix/event" "maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "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. // 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{ return newUIMessage(evt, displayname, &ExpandedTextMessage{
Text: text, Text: text,
}) })

View File

@ -22,8 +22,9 @@ import (
"image" "image"
"image/color" "image/color"
"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/id"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -35,10 +36,10 @@ import (
) )
type FileMessage struct { type FileMessage struct {
Type mautrix.MessageType Type event.MessageType
Body string Body string
URL mautrix.ContentURI URL id.ContentURI
Thumbnail mautrix.ContentURI Thumbnail id.ContentURI
imageData []byte imageData []byte
buffer []tstring.TString buffer []tstring.TString
@ -46,9 +47,9 @@ type FileMessage struct {
} }
// NewFileMessage creates a new FileMessage object with the provided values and the default state. // 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 { func NewFileMessage(matrix ifc.MatrixContainer, evt *muksevt.Event, displayname string) *UIMessage {
url, _ := mautrix.ParseContentURI(evt.Content.URL) url, _ := evt.Content.URL.Parse()
thumbnail, _ := mautrix.ParseContentURI(evt.Content.GetInfo().ThumbnailURL) thumbnail, _ := evt.Content.GetInfo().ThumbnailURL.Parse()
return newUIMessage(evt, displayname, &FileMessage{ return newUIMessage(evt, displayname, &FileMessage{
Type: evt.Content.MsgType, Type: evt.Content.MsgType,
Body: evt.Content.Body, Body: evt.Content.Body,
@ -72,13 +73,13 @@ func (msg *FileMessage) Clone() MessageRenderer {
func (msg *FileMessage) NotificationContent() string { func (msg *FileMessage) NotificationContent() string {
switch msg.Type { switch msg.Type {
case mautrix.MsgImage: case event.MsgImage:
return "Sent an image" return "Sent an image"
case mautrix.MsgAudio: case event.MsgAudio:
return "Sent an audio file" return "Sent an audio file"
case mautrix.MsgVideo: case event.MsgVideo:
return "Sent a video" return "Sent a video"
case mautrix.MsgFile: case event.MsgFile:
fallthrough fallthrough
default: default:
return "Sent a file" return "Sent a file"
@ -96,7 +97,7 @@ func (msg *FileMessage) String() string {
func (msg *FileMessage) DownloadPreview() { func (msg *FileMessage) DownloadPreview() {
url := msg.Thumbnail url := msg.Thumbnail
if url.IsEmpty() { if url.IsEmpty() {
if msg.Type == mautrix.MsgImage && !msg.URL.IsEmpty() { if msg.Type == event.MsgImage && !msg.URL.IsEmpty() {
msg.Thumbnail = msg.URL msg.Thumbnail = msg.URL
url = msg.Thumbnail url = msg.Thumbnail
} else { } else {

View File

@ -27,8 +27,9 @@ import (
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"golang.org/x/net/html" "golang.org/x/net/html"
"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/id"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
@ -185,12 +186,12 @@ func (parser *htmlParser) linkToEntity(node *html.Node) Entity {
pillTarget := match[1] pillTarget := match[1]
text := NewTextEntity(pillTarget) text := NewTextEntity(pillTarget)
if pillTarget[0] == '@' { 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.Text = member.Displayname
text.Style = text.Style.Foreground(widget.GetHashColor(pillTarget)) text.Style = text.Style.Foreground(widget.GetHashColor(pillTarget))
} }
entity.Children = []Entity{text} entity.Children = []Entity{text}
/*} else if slash := strings.IndexRune(pillTarget, '/'); slash != -1 { /*} else if slash := strings.IndexRune(pillTarget, '/'); slash != -1 {
room := pillTarget[:slash] room := pillTarget[:slash]
event := pillTarget[slash+1:]*/ event := pillTarget[slash+1:]*/
} else if pillTarget[0] == '#' { } else if pillTarget[0] == '#' {
@ -383,9 +384,9 @@ func (parser *htmlParser) Parse(htmlData string) Entity {
const TabLength = 4 const TabLength = 4
// Parse parses a HTML-formatted Matrix event into a UIMessage. // 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 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(html.EscapeString(evt.Content.Body), "\n", "<br/>", -1)
} }
htmlData = strings.Replace(htmlData, "\t", strings.Repeat(" ", TabLength), -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{ root = &ContainerEntity{
BaseEntity: &BaseEntity{ BaseEntity: &BaseEntity{
Tag: "emote", Tag: "emote",

View File

@ -17,7 +17,7 @@
package messages package messages
import ( import (
"maunium.net/go/gomuks/matrix/event" "maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -32,7 +32,7 @@ type HTMLMessage struct {
focused bool 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{ return newUIMessage(evt, displayname, &HTMLMessage{
Root: root, Root: root,
}) })

View File

@ -20,8 +20,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"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/id"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
@ -31,7 +32,7 @@ import (
"maunium.net/go/gomuks/ui/widget" "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 roomView := mainView.GetRoom(roomID); roomView != nil {
if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil { if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil {
if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil { if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil {
@ -42,7 +43,7 @@ func getCachedEvent(mainView ifc.MainView, roomID, eventID string) *UIMessage {
return nil 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) msg := directParseEvent(matrix, room, evt)
if msg == nil { if msg == nil {
return nil return nil
@ -64,36 +65,36 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R
return msg return msg
} }
func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event) *UIMessage { func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *muksevt.Event) *UIMessage {
displayname := evt.Sender displayname := string(evt.Sender)
member := room.GetMember(evt.Sender) member := room.GetMember(evt.Sender)
if member != nil { if member != nil {
displayname = member.Displayname 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) return NewRedactedMessage(evt, displayname)
} }
switch evt.Type { switch evt.Type {
case mautrix.EventSticker: case event.EventSticker:
evt.Content.MsgType = mautrix.MsgImage evt.Content.MsgType = event.MsgImage
fallthrough fallthrough
case mautrix.EventMessage: case event.EventMessage:
return ParseMessage(matrix, room, evt, displayname) 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))) 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) return ParseStateEvent(evt, displayname)
case mautrix.StateMember: case event.StateMember:
return ParseMembershipEvent(room, evt) return ParseMembershipEvent(room, evt)
} }
return nil 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)) text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender))
switch evt.Type { switch evt.Type {
case mautrix.StateTopic: case event.StateTopic:
if len(evt.Content.Topic) == 0 { if len(evt.Content.Topic) == 0 {
text = text.AppendColor(" removed the topic.", tcell.ColorGreen) text = text.AppendColor(" removed the topic.", tcell.ColorGreen)
} else { } else {
@ -101,7 +102,7 @@ func ParseStateEvent(evt *event.Event, displayname string) *UIMessage {
AppendStyle(evt.Content.Topic, tcell.StyleDefault.Underline(true)). AppendStyle(evt.Content.Topic, tcell.StyleDefault.Underline(true)).
AppendColor(".", tcell.ColorGreen) AppendColor(".", tcell.ColorGreen)
} }
case mautrix.StateRoomName: case event.StateRoomName:
if len(evt.Content.Name) == 0 { if len(evt.Content.Name) == 0 {
text = text.AppendColor(" removed the room name.", tcell.ColorGreen) text = text.AppendColor(" removed the room name.", tcell.ColorGreen)
} else { } else {
@ -109,21 +110,21 @@ func ParseStateEvent(evt *event.Event, displayname string) *UIMessage {
AppendStyle(evt.Content.Name, tcell.StyleDefault.Underline(true)). AppendStyle(evt.Content.Name, tcell.StyleDefault.Underline(true)).
AppendColor(".", tcell.ColorGreen) AppendColor(".", tcell.ColorGreen)
} }
case mautrix.StateCanonicalAlias: case event.StateCanonicalAlias:
if len(evt.Content.Alias) == 0 { if len(evt.Content.Alias) == 0 {
text = text.AppendColor(" removed the main address of the room.", tcell.ColorGreen) text = text.AppendColor(" removed the main address of the room.", tcell.ColorGreen)
} else { } else {
text = text.AppendColor(" changed the main address of the room to ", tcell.ColorGreen). 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) AppendColor(".", tcell.ColorGreen)
} }
case mautrix.StateAliases: //case event.StateAliases:
text = ParseAliasEvent(evt, displayname) // text = ParseAliasEvent(evt, displayname)
} }
return NewExpandedTextMessage(evt, displayname, text) 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 { if len(evt.Content.GetReplyTo()) > 0 {
evt.Content.RemoveReplyFallback() 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 evt.Content = *evt.Gomuks.Edits[len(evt.Gomuks.Edits)-1].Content.NewContent
} }
switch evt.Content.MsgType { switch evt.Content.MsgType {
case mautrix.MsgText, mautrix.MsgNotice, mautrix.MsgEmote: case event.MsgText, event.MsgNotice, event.MsgEmote:
if evt.Content.Format == mautrix.FormatHTML { if evt.Content.Format == event.FormatHTML {
return NewHTMLMessage(evt, displayname, html.Parse(room, evt, displayname)) return NewHTMLMessage(evt, displayname, html.Parse(room, evt, displayname))
} }
evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1) evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1)
return NewTextMessage(evt, displayname, evt.Content.Body) 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) msg := NewFileMessage(matrix, evt, displayname)
if !matrix.Preferences().DisableDownloads { if !matrix.Preferences().DisableDownloads {
renderer := msg.Renderer.(*FileMessage) renderer := msg.Renderer.(*FileMessage)
@ -148,54 +149,54 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event
return nil 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 { switch membership {
case "invite": case "invite":
sender = "---" sender = "---"
text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", senderDisplayname, displayname), tcell.ColorGreen) text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", senderDisplayname, displayname), tcell.ColorGreen)
text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) 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": case "join":
sender = "-->" sender = "-->"
if prevMembership == mautrix.MembershipInvite { if prevMembership == event.MembershipInvite {
text = tstring.NewColorTString(fmt.Sprintf("%s accepted the invite.", displayname), tcell.ColorGreen) text = tstring.NewColorTString(fmt.Sprintf("%s accepted the invite.", displayname), tcell.ColorGreen)
} else { } else {
text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) 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": case "leave":
sender = "<--" sender = "<--"
if evt.Sender != *evt.StateKey { if evt.Sender != id.UserID(*evt.StateKey) {
if prevMembership == mautrix.MembershipBan { if prevMembership == event.MembershipBan {
text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen) 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 { } else {
text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) 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)) text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender))
} else { } else {
if displayname == *evt.StateKey { if displayname == *evt.StateKey {
displayname = prevDisplayname displayname = prevDisplayname
} }
if prevMembership == mautrix.MembershipInvite { if prevMembership == event.MembershipInvite {
text = tstring.NewColorTString(fmt.Sprintf("%s rejected the invite.", displayname), tcell.ColorRed) text = tstring.NewColorTString(fmt.Sprintf("%s rejected the invite.", displayname), tcell.ColorRed)
} else { } else {
text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) 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": case "ban":
text = tstring.NewColorTString(fmt.Sprintf("%s banned %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) 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)) text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender))
} }
return 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) member := room.GetMember(evt.Sender)
senderDisplayname := evt.Sender senderDisplayname := string(evt.Sender)
if member != nil { if member != nil {
senderDisplayname = member.Displayname senderDisplayname = member.Displayname
} }
@ -206,7 +207,7 @@ func getMembershipEventContent(room *rooms.Room, evt *event.Event) (sender strin
displayname = *evt.StateKey displayname = *evt.StateKey
} }
prevMembership := mautrix.MembershipLeave prevMembership := event.MembershipLeave
prevDisplayname := *evt.StateKey prevDisplayname := *evt.StateKey
if evt.Unsigned.PrevContent != nil { if evt.Unsigned.PrevContent != nil {
prevMembership = evt.Unsigned.PrevContent.Membership 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) sender, text = getMembershipChangeMessage(evt, membership, prevMembership, senderDisplayname, displayname, prevDisplayname)
} else if displayname != prevDisplayname { } else if displayname != prevDisplayname {
sender = "---" sender = "---"
color := widget.GetHashColor(*evt.StateKey) color := widget.GetHashColor(evt.StateKey)
text = tstring.NewBlankTString(). text = tstring.NewBlankTString().
AppendColor(prevDisplayname, color). AppendColor(prevDisplayname, color).
AppendColor(" changed their display name to ", tcell.ColorGreen). AppendColor(" changed their display name to ", tcell.ColorGreen).
@ -230,7 +231,7 @@ func getMembershipEventContent(room *rooms.Room, evt *event.Event) (sender strin
return return
} }
func ParseMembershipEvent(room *rooms.Room, evt *event.Event) *UIMessage { func ParseMembershipEvent(room *rooms.Room, evt *muksevt.Event) *UIMessage {
displayname, text := getMembershipEventContent(room, evt) displayname, text := getMembershipEventContent(room, evt)
if len(text) == 0 { if len(text) == 0 {
return nil return nil
@ -239,65 +240,65 @@ func ParseMembershipEvent(room *rooms.Room, evt *event.Event) *UIMessage {
return NewExpandedTextMessage(evt, displayname, text) return NewExpandedTextMessage(evt, displayname, text)
} }
func ParseAliasEvent(evt *event.Event, displayname string) tstring.TString { //func ParseAliasEvent(evt *muksevt.Event, displayname string) tstring.TString {
var prevAliases []string // var prevAliases []string
if evt.Unsigned.PrevContent != nil { // if evt.Unsigned.PrevContent != nil {
prevAliases = evt.Unsigned.PrevContent.Aliases // prevAliases = evt.Unsigned.PrevContent.Aliases
} // }
aliases := evt.Content.Aliases // aliases := evt.Content.Aliases
var added, removed []tstring.TString // var added, removed []tstring.TString
Outer1: //Outer1:
for _, oldAlias := range prevAliases { // for _, oldAlias := range prevAliases {
for _, newAlias := range aliases { // for _, newAlias := range aliases {
if oldAlias == newAlias { // if oldAlias == newAlias {
continue Outer1 // continue Outer1
} // }
} // }
removed = append(removed, tstring.NewStyleTString(oldAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true))) // removed = append(removed, tstring.NewStyleTString(oldAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true)))
} // }
Outer2: //Outer2:
for _, newAlias := range aliases { // for _, newAlias := range aliases {
for _, oldAlias := range prevAliases { // for _, oldAlias := range prevAliases {
if oldAlias == newAlias { // if oldAlias == newAlias {
continue Outer2 // continue Outer2
} // }
} // }
added = append(added, tstring.NewStyleTString(newAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true))) // added = append(added, tstring.NewStyleTString(newAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true)))
} // }
var addedStr, removedStr tstring.TString // var addedStr, removedStr tstring.TString
if len(added) == 1 { // if len(added) == 1 {
addedStr = added[0] // addedStr = added[0]
} else if len(added) > 1 { // } else if len(added) > 1 {
addedStr = tstring. // addedStr = tstring.
Join(added[:len(added)-1], ", "). // Join(added[:len(added)-1], ", ").
Append(" and "). // Append(" and ").
AppendTString(added[len(added)-1]) // AppendTString(added[len(added)-1])
} // }
if len(removed) == 1 { // if len(removed) == 1 {
removedStr = removed[0] // removedStr = removed[0]
} else if len(removed) > 1 { // } else if len(removed) > 1 {
removedStr = tstring. // removedStr = tstring.
Join(removed[:len(removed)-1], ", "). // Join(removed[:len(removed)-1], ", ").
Append(" and "). // Append(" and ").
AppendTString(removed[len(removed)-1]) // AppendTString(removed[len(removed)-1])
} // }
text := tstring.NewBlankTString() // text := tstring.NewBlankTString()
if len(addedStr) > 0 && len(removedStr) > 0 { // if len(addedStr) > 0 && len(removedStr) > 0 {
text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). // text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen).
AppendTString(addedStr). // AppendTString(addedStr).
AppendColor(" and removed ", tcell.ColorGreen). // AppendColor(" and removed ", tcell.ColorGreen).
AppendTString(removedStr). // AppendTString(removedStr).
AppendColor(" as addresses for this room.", tcell.ColorGreen) // AppendColor(" as addresses for this room.", tcell.ColorGreen)
} else if len(addedStr) > 0 { // } else if len(addedStr) > 0 {
text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). // text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen).
AppendTString(addedStr). // AppendTString(addedStr).
AppendColor(" as addresses for this room.", tcell.ColorGreen) // AppendColor(" as addresses for this room.", tcell.ColorGreen)
} else if len(removedStr) > 0 { // } else if len(removedStr) > 0 {
text = text.AppendColor(fmt.Sprintf("%s removed ", displayname), tcell.ColorGreen). // text = text.AppendColor(fmt.Sprintf("%s removed ", displayname), tcell.ColorGreen).
AppendTString(removedStr). // AppendTString(removedStr).
AppendColor(" as addresses for this room.", tcell.ColorGreen) // AppendColor(" as addresses for this room.", tcell.ColorGreen)
} else { // } else {
return nil // return nil
} // }
return text // return text
} //}

View File

@ -17,7 +17,7 @@
package messages package messages
import ( import (
"maunium.net/go/gomuks/matrix/event" "maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -26,7 +26,7 @@ import (
type RedactedMessage struct{} type RedactedMessage struct{}
func NewRedactedMessage(evt *event.Event, displayname string) *UIMessage { func NewRedactedMessage(evt *muksevt.Event, displayname string) *UIMessage {
return newUIMessage(evt, displayname, &RedactedMessage{}) return newUIMessage(evt, displayname, &RedactedMessage{})
} }

View File

@ -20,7 +20,7 @@ import (
"fmt" "fmt"
"time" "time"
"maunium.net/go/gomuks/matrix/event" "maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/gomuks/config" "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. // 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{ return newUIMessage(evt, displayname, &TextMessage{
Text: text, Text: text,
}) })

View File

@ -24,6 +24,7 @@ import (
sync "github.com/sasha-s/go-deadlock" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/mautrix/id"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -105,7 +106,7 @@ func NewRoomList(parent *MainView) *RoomList {
return list return list
} }
func (list *RoomList) Contains(roomID string) bool { func (list *RoomList) Contains(roomID id.RoomID) bool {
list.RLock() list.RLock()
defer list.RUnlock() defer list.RUnlock()
for _, trl := range list.items { for _, trl := range list.items {

View File

@ -26,18 +26,19 @@ import (
"github.com/kyokomi/emoji" "github.com/kyokomi/emoji"
"github.com/mattn/go-runewidth" "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/mauview"
"maunium.net/go/mautrix"
"maunium.net/go/tcell" "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/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/open"
"maunium.net/go/gomuks/lib/util" "maunium.net/go/gomuks/lib/util"
"maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
@ -72,9 +73,9 @@ type RoomView struct {
selectReason SelectReason selectReason SelectReason
selectContent string selectContent string
replying *event.Event replying *muksevt.Event
editing *event.Event editing *muksevt.Event
editMoveText string editMoveText string
completions struct { completions struct {
@ -186,7 +187,7 @@ func (view *RoomView) OnSelect(message *messages.UIMessage) {
case SelectReply: case SelectReply:
view.replying = message.Event view.replying = message.Event
if len(view.selectContent) > 0 { if len(view.selectContent) > 0 {
go view.SendMessage(mautrix.MsgText, view.selectContent) go view.SendMessage(event.MsgText, view.selectContent)
} }
case SelectReact: case SelectReact:
go view.SendReaction(message.EventID, view.selectContent) go view.SendReaction(message.EventID, view.selectContent)
@ -217,7 +218,7 @@ func (view *RoomView) GetStatus() string {
buf.WriteString("Editing message - ") buf.WriteString("Editing message - ")
} else if view.replying != nil { } else if view.replying != nil {
buf.WriteString("Replying to ") buf.WriteString("Replying to ")
buf.WriteString(view.replying.Sender) buf.WriteString(string(view.replying.Sender))
buf.WriteString(" - ") buf.WriteString(" - ")
} else if view.selecting { } else if view.selecting {
buf.WriteString("Selecting message to ") buf.WriteString("Selecting message to ")
@ -235,12 +236,19 @@ func (view *RoomView) GetStatus() string {
} }
if len(view.typing) == 1 { if len(view.typing) == 1 {
buf.WriteString("Typing: " + view.typing[0]) buf.WriteString("Typing: " + string(view.typing[0]))
buf.WriteString(" - ") buf.WriteString(" - ")
} else if len(view.typing) > 1 { } else if len(view.typing) > 1 {
_, _ = fmt.Fprintf(&buf, buf.WriteString("Typing: ")
"Typing: %s and %s - ", for i, userID := range view.typing {
strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1]) 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(), " - ") return strings.TrimSuffix(buf.String(), " - ")
@ -384,15 +392,18 @@ func (view *RoomView) SetCompletions(completions []string) {
func (view *RoomView) loadTyping() { func (view *RoomView) loadTyping() {
for index, user := range view.typing { for index, user := range view.typing {
member := view.Room.GetMember(user) member := view.Room.GetMember(id.UserID(user))
if member != nil { if member != nil {
view.typing[index] = member.Displayname view.typing[index] = member.Displayname
} }
} }
} }
func (view *RoomView) SetTyping(users []string) { func (view *RoomView) SetTyping(users []id.UserID) {
view.typing = users view.typing = make([]string, len(users))
for i, user := range users {
view.typing[i] = string(user)
}
if view.Room.Loaded() { if view.Room.Loaded() {
view.loadTyping() view.loadTyping()
} }
@ -406,13 +417,13 @@ type completion struct {
func (view *RoomView) autocompleteUser(existingText string) (completions []completion) { func (view *RoomView) autocompleteUser(existingText string) (completions []completion) {
textWithoutPrefix := strings.TrimPrefix(existingText, "@") textWithoutPrefix := strings.TrimPrefix(existingText, "@")
for userID, user := range view.Room.GetMembers() { for userID, user := range view.Room.GetMembers() {
if user.Displayname == textWithoutPrefix || userID == existingText { if user.Displayname == textWithoutPrefix || string(userID) == existingText {
// Exact match, return that. // 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) { if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) {
completions = append(completions, completion{user.Displayname, userID}) completions = append(completions, completion{user.Displayname, string(userID)})
} }
} }
return return
@ -420,13 +431,13 @@ func (view *RoomView) autocompleteUser(existingText string) (completions []compl
func (view *RoomView) autocompleteRoom(existingText string) (completions []completion) { func (view *RoomView) autocompleteRoom(existingText string) (completions []completion) {
for _, room := range view.parent.rooms { for _, room := range view.parent.rooms {
alias := room.Room.GetCanonicalAlias() alias := string(room.Room.GetCanonicalAlias())
if alias == existingText { if alias == existingText {
// Exact match, return that. // Exact match, return that.
return []completion{{alias, room.Room.ID}} return []completion{{alias, string(room.Room.ID)}}
} }
if strings.HasPrefix(alias, existingText) { if strings.HasPrefix(alias, existingText) {
completions = append(completions, completion{alias, room.Room.ID}) completions = append(completions, completion{alias, string(room.Room.ID)})
continue continue
} }
} }
@ -457,7 +468,7 @@ func (view *RoomView) autocompleteEmoji(word string) (completions []string) {
return return
} }
func (view *RoomView) SetEditing(evt *event.Event) { func (view *RoomView) SetEditing(evt *muksevt.Event) {
if evt == nil { if evt == nil {
view.editing = nil view.editing = nil
view.SetInputText(view.editMoveText) 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 // replying should never be non-nil when SetEditing, but do this just to be safe
view.replying = nil view.replying = nil
text := view.editing.Content.Body text := view.editing.Content.Body
if view.editing.Content.MsgType == mautrix.MsgEmote { if view.editing.Content.MsgType == event.MsgEmote {
text = "/me " + text text = "/me " + text
} }
view.input.SetText(text) view.input.SetText(text)
@ -479,21 +490,21 @@ func (view *RoomView) SetEditing(evt *event.Event) {
view.input.SetCursorOffset(-1) 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 { func (view *RoomView) filterOwnOnly(evt *muksevt.Event) bool {
return evt.Sender == view.parent.matrix.Client().UserID && evt.Type == mautrix.EventMessage return evt.Sender == view.parent.matrix.Client().UserID && evt.Type == event.EventMessage
} }
func (view *RoomView) filterMediaOnly(evt *event.Event) bool { func (view *RoomView) filterMediaOnly(evt *muksevt.Event) bool {
return evt.Type == mautrix.EventMessage && ( return evt.Type == event.EventMessage && (
evt.Content.MsgType == mautrix.MsgFile || evt.Content.MsgType == event.MsgFile ||
evt.Content.MsgType == mautrix.MsgImage || evt.Content.MsgType == event.MsgImage ||
evt.Content.MsgType == mautrix.MsgAudio || evt.Content.MsgType == event.MsgAudio ||
evt.Content.MsgType == mautrix.MsgVideo) 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 currentFound := current == nil
msgs := view.MessageView().messages msgs := view.MessageView().messages
for i := 0; i < len(msgs); i++ { 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 index = len(msgs) - i - 1
} }
evt := msgs[index] evt := msgs[index]
if evt.EventID == "" || evt.EventID == evt.TxnID || evt.IsService { if evt.EventID == "" || string(evt.EventID) == evt.TxnID || evt.IsService {
continue continue
} else if currentFound { } else if currentFound {
if allow == nil || allow(evt.Event) { 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 { } else if cmd := view.parent.cmdProcessor.ParseCommand(view, text); cmd != nil {
go view.parent.cmdProcessor.HandleCommand(cmd) go view.parent.cmdProcessor.HandleCommand(cmd)
} else { } else {
go view.SendMessage(mautrix.MsgText, text) go view.SendMessage(event.MsgText, text)
} }
view.editMoveText = "" view.editMoveText = ""
view.SetInputText("") 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) path, err := view.parent.matrix.DownloadToDisk(url, filename)
if err != nil { if err != nil {
view.AddServiceMessage(fmt.Sprintf("Failed to download media: %v", err)) 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() defer debug.Recover()
err := view.parent.matrix.Redact(view.Room.ID, eventID, reason) err := view.parent.matrix.Redact(view.Room.ID, eventID, reason)
if err != nil { 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() defer debug.Recover()
debug.Print("Reacting to", eventID, "in", view.Room.ID, "with", reaction) debug.Print("Reacting to", eventID, "in", view.Room.ID, "with", reaction)
eventID, err := view.parent.matrix.SendEvent(&event.Event{ eventID, err := view.parent.matrix.SendEvent(&muksevt.Event{
Event: &mautrix.Event{ Event: &event.Event{
Type: mautrix.EventReaction, Type: event.EventReaction,
RoomID: view.Room.ID, RoomID: view.Room.ID,
Content: mautrix.Content{ Content: event.Content{
RelatesTo: &mautrix.RelatesTo{ RelatesTo: &event.RelatesTo{
Type: mautrix.RelAnnotation, Type: event.RelAnnotation,
EventID: eventID, EventID: eventID,
Key: reaction, 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, "") 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() defer debug.Recover()
debug.Print("Sending message", msgtype, text, "to", view.Room.ID) debug.Print("Sending message", msgtype, text, "to", view.Room.ID)
if !view.config.Preferences.DisableEmojis { if !view.config.Preferences.DisableEmojis {
@ -683,12 +694,12 @@ func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html st
var rel *ifc.Relation var rel *ifc.Relation
if view.editing != nil { if view.editing != nil {
rel = &ifc.Relation{ rel = &ifc.Relation{
Type: mautrix.RelReplace, Type: event.RelReplace,
Event: view.editing, Event: view.editing,
} }
} else if view.replying != nil { } else if view.replying != nil {
rel = &ifc.Relation{ rel = &ifc.Relation{
Type: mautrix.RelReference, Type: event.RelReference,
Event: view.replying, Event: view.replying,
} }
} }
@ -699,7 +710,7 @@ func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html st
view.status.SetText(view.GetStatus()) view.status.SetText(view.GetStatus())
eventID, err := view.parent.matrix.SendEvent(evt) eventID, err := view.parent.matrix.SendEvent(evt)
if err != nil { if err != nil {
msg.State = event.StateSendFail msg.State = muksevt.StateSendFail
// Show shorter version if available // Show shorter version if available
if httpErr, ok := err.(mautrix.HTTPError); ok { if httpErr, ok := err.(mautrix.HTTPError); ok {
err = httpErr err = httpErr
@ -712,7 +723,7 @@ func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html st
} else { } else {
debug.Print("Event ID received:", eventID) debug.Print("Event ID received:", eventID)
msg.EventID = eventID msg.EventID = eventID
msg.State = event.StateDefault msg.State = muksevt.StateDefault
view.MessageView().setMessageID(msg) view.MessageView().setMessageID(msg)
view.parent.parent.Render() view.parent.parent.Render()
} }
@ -734,8 +745,8 @@ func (view *RoomView) Update() {
} }
func (view *RoomView) UpdateUserList() { func (view *RoomView) UpdateUserList() {
pls := &mautrix.PowerLevels{} pls := &event.PowerLevels{}
if plEvent := view.Room.GetStateEvent(mautrix.StatePowerLevels, ""); plEvent != nil { if plEvent := view.Room.GetStateEvent(event.StatePowerLevels, ""); plEvent != nil {
pls = plEvent.Content.GetPowerLevels() pls = plEvent.Content.GetPowerLevels()
} }
view.userList.Update(view.Room.GetMembers(), pls) view.userList.Update(view.Room.GetMembers(), pls)
@ -746,17 +757,17 @@ func (view *RoomView) AddServiceMessage(text string) {
view.content.AddMessage(messages.NewServiceMessage(text), AppendMessage) 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) 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 { if msg := view.parseEvent(evt); msg != nil {
view.content.AddMessage(msg, PrependMessage) 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 { if msg := view.parseEvent(evt); msg != nil {
view.content.AddMessage(msg, AppendMessage) view.content.AddMessage(msg, AppendMessage)
return msg return msg
@ -764,17 +775,17 @@ func (view *RoomView) AddEvent(evt *event.Event) ifc.Message {
return nil return nil
} }
func (view *RoomView) AddRedaction(redactedEvt *event.Event) { func (view *RoomView) AddRedaction(redactedEvt *muksevt.Event) {
view.AddEvent(redactedEvt) view.AddEvent(redactedEvt)
} }
func (view *RoomView) AddEdit(evt *event.Event) { func (view *RoomView) AddEdit(evt *muksevt.Event) {
if msg := view.parseEvent(evt); msg != nil { if msg := view.parseEvent(evt); msg != nil {
view.content.AddMessage(msg, IgnoreMessage) 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() msgView := view.MessageView()
msg := msgView.getMessageByID(evt.ID) msg := msgView.getMessageByID(evt.ID)
if msg == nil { 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] message, ok := view.content.messageIDs[eventID]
if !ok { if !ok {
return nil return nil

View File

@ -19,6 +19,7 @@ package ui
import ( import (
"math" "math"
"maunium.net/go/mautrix/id"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
@ -75,7 +76,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
hs := ui.gmx.Config().HS hs := ui.gmx.Config().HS
view.homeserver.SetPlaceholder("https://example.com").SetText(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.password.SetPlaceholder("correct horse battery staple").SetMaskCharacter('*')
view.quitButton.SetOnClick(func() { ui.gmx.Stop(true) }).SetBackgroundColor(tcell.ColorDarkCyan) 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() { func (view *LoginView) resolveWellKnown() {
_, homeserver, err := mautrix.ParseUserID(view.username.GetText()) _, homeserver, err := id.UserID(view.username.GetText()).Parse()
if err != nil { if err != nil {
return return
} }

View File

@ -27,6 +27,7 @@ import (
sync "github.com/sasha-s/go-deadlock" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/mautrix/id"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -34,9 +35,9 @@ import (
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/notification" "maunium.net/go/gomuks/lib/notification"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/mautrix/pushrules"
) )
type MainView struct { type MainView struct {
@ -45,7 +46,7 @@ type MainView struct {
roomList *RoomList roomList *RoomList
roomView *mauview.Box roomView *mauview.Box
currentRoom *RoomView currentRoom *RoomView
rooms map[string]*RoomView rooms map[id.RoomID]*RoomView
roomsLock sync.RWMutex roomsLock sync.RWMutex
cmdProcessor *CommandProcessor cmdProcessor *CommandProcessor
focused mauview.Focusable focused mauview.Focusable
@ -64,7 +65,7 @@ func (ui *GomuksUI) NewMainView() mauview.Component {
mainView := &MainView{ mainView := &MainView{
flex: mauview.NewFlex().SetDirection(mauview.FlexColumn), flex: mauview.NewFlex().SetDirection(mauview.FlexColumn),
roomView: mauview.NewBox(nil).SetBorder(false), roomView: mauview.NewBox(nil).SetBorder(false),
rooms: make(map[string]*RoomView), rooms: make(map[id.RoomID]*RoomView),
matrix: ui.gmx.Matrix(), matrix: ui.gmx.Matrix(),
gmx: ui.gmx, gmx: ui.gmx,
@ -295,7 +296,7 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
return nil 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) room, ok := view.getRoomView(roomID, true)
if !ok { if !ok {
return view.addRoom(view.matrix.GetOrCreateRoom(roomID)) return view.addRoom(view.matrix.GetOrCreateRoom(roomID))
@ -303,7 +304,7 @@ func (view *MainView) GetRoom(roomID string) ifc.RoomView {
return room 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 { if lock {
view.roomsLock.RLock() view.roomsLock.RLock()
room, ok = view.rooms[roomID] room, ok = view.rooms[roomID]
@ -357,7 +358,7 @@ func (view *MainView) addRoom(room *rooms.Room) *RoomView {
func (view *MainView) SetRooms(rooms *rooms.RoomCache) { func (view *MainView) SetRooms(rooms *rooms.RoomCache) {
view.roomList.Clear() view.roomList.Clear()
view.roomsLock.Lock() view.roomsLock.Lock()
view.rooms = make(map[string]*RoomView) view.rooms = make(map[id.RoomID]*RoomView)
for _, room := range rooms.Map { for _, room := range rooms.Map {
if room.HasLeft { if room.HasLeft {
continue continue
@ -383,7 +384,7 @@ func (view *MainView) UpdateTags(room *rooms.Room) {
view.parent.Render() 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) roomView, ok := view.getRoomView(roomID, true)
if ok { if ok {
roomView.SetTyping(users) roomView.SetTyping(users)
@ -438,7 +439,7 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul
message.SetIsHighlight(should.Highlight) message.SetIsHighlight(should.Highlight)
} }
func (view *MainView) LoadHistory(roomID string) { func (view *MainView) LoadHistory(roomID id.RoomID) {
defer debug.Recover() defer debug.Recover()
roomView, ok := view.getRoomView(roomID, true) roomView, ok := view.getRoomView(roomID, true)
if !ok { if !ok {

View File

@ -21,6 +21,8 @@ import (
"hash/fnv" "hash/fnv"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/mautrix/id"
) )
var colorNames = []string{ var colorNames = []string{
@ -201,8 +203,17 @@ func GetHashColorName(s string) string {
// GetHashColor gets the tcell Color value for the given string. // GetHashColor gets the tcell Color value for the given string.
// //
// GetHashColor calls GetHashColorName() and gets the Color value from the tcell.ColorNames map. // GetHashColor calls GetHashColorName() and gets the Color value from the tcell.ColorNames map.
func GetHashColor(s string) tcell.Color { func GetHashColor(val interface{}) tcell.Color {
return tcell.ColorNames[GetHashColorName(s)] 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. // AddColor adds tview color tags to the given string.