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 (
sync "github.com/sasha-s/go-deadlock"
bolt "go.etcd.io/bbolt"
"maunium.net/go/gomuks/matrix/event"
"maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type HistoryManager struct {
@ -87,7 +88,7 @@ func (hm *HistoryManager) Close() error {
var (
EventNotFoundError = errors.New("event not found")
RoomNotFoundError = errors.New("room not found")
RoomNotFoundError = errors.New("room not found")
)
func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []byte) (*bolt.Bucket, []byte, error) {
@ -103,7 +104,7 @@ func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []b
return stream, index, nil
}
func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*event.Event, error) {
func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*muksevt.Event, error) {
eventData := stream.Get(index)
if eventData == nil || len(eventData) == 0 {
return nil, EventNotFoundError
@ -111,7 +112,7 @@ func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byt
return unmarshalEvent(eventData)
}
func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Event, err error) {
func (hm *HistoryManager) Get(room *rooms.Room, eventID id.EventID) (evt *muksevt.Event, err error) {
err = hm.db.View(func(tx *bolt.Tx) error {
if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil {
return err
@ -123,7 +124,7 @@ func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Even
return
}
func (hm *HistoryManager) Update(room *rooms.Room, eventID string, update func(evt *event.Event) error) error {
func (hm *HistoryManager) Update(room *rooms.Room, eventID id.EventID, update func(evt *muksevt.Event) error) error {
return hm.db.Update(func(tx *bolt.Tx) error {
if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil {
return err
@ -140,18 +141,18 @@ func (hm *HistoryManager) Update(room *rooms.Room, eventID string, update func(e
})
}
func (hm *HistoryManager) Append(room *rooms.Room, events []*mautrix.Event) ([]*event.Event, error) {
func (hm *HistoryManager) Append(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) {
return hm.store(room, events, true)
}
func (hm *HistoryManager) Prepend(room *rooms.Room, events []*mautrix.Event) ([]*event.Event, error) {
func (hm *HistoryManager) Prepend(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) {
return hm.store(room, events, false)
}
func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, append bool) ([]*event.Event, error) {
func (hm *HistoryManager) store(room *rooms.Room, events []*event.Event, append bool) ([]*muksevt.Event, error) {
hm.Lock()
defer hm.Unlock()
newEvents := make([]*event.Event, len(events))
newEvents := make([]*muksevt.Event, len(events))
err := hm.db.Update(func(tx *bolt.Tx) error {
streamPointers := tx.Bucket(bucketStreamPointers)
rid := []byte(room.ID)
@ -177,7 +178,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen
return err
}
for i, evt := range events {
newEvents[i] = event.Wrap(evt)
newEvents[i] = muksevt.Wrap(evt)
if err := put(stream, eventIDs, newEvents[i], ptrStart+uint64(i)); err != nil {
return err
}
@ -198,7 +199,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen
}
eventCount := uint64(len(events))
for i, evt := range events {
newEvents[i] = event.Wrap(evt)
newEvents[i] = muksevt.Wrap(evt)
if err := put(stream, eventIDs, newEvents[i], -ptrStart-uint64(i)); err != nil {
return err
}
@ -215,12 +216,11 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen
return newEvents, err
}
func (hm *HistoryManager) Load(room *rooms.Room, num int) (events []*event.Event, err error) {
func (hm *HistoryManager) Load(room *rooms.Room, num int) (events []*muksevt.Event, err error) {
hm.Lock()
defer hm.Unlock()
err = hm.db.View(func(tx *bolt.Tx) error {
rid := []byte(room.ID)
stream := tx.Bucket(bucketRoomStreams).Bucket(rid)
stream := tx.Bucket(bucketRoomStreams).Bucket([]byte(room.ID))
if stream == nil {
return nil
}
@ -265,7 +265,7 @@ func btoi(b []byte) uint64 {
return binary.BigEndian.Uint64(b)
}
func marshalEvent(evt *event.Event) ([]byte, error) {
func marshalEvent(evt *muksevt.Event) ([]byte, error) {
var buf bytes.Buffer
enc := gzip.NewWriter(&buf)
if err := gob.NewEncoder(enc).Encode(evt); err != nil {
@ -277,8 +277,8 @@ func marshalEvent(evt *event.Event) ([]byte, error) {
return buf.Bytes(), nil
}
func unmarshalEvent(data []byte) (*event.Event, error) {
evt := &event.Event{}
func unmarshalEvent(data []byte) (*muksevt.Event, error) {
evt := &muksevt.Event{}
if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil {
return nil, err
} else if err := gob.NewDecoder(cmpReader).Decode(evt); err != nil {
@ -290,7 +290,7 @@ func unmarshalEvent(data []byte) (*event.Event, error) {
return evt, nil
}
func put(streams, eventIDs *bolt.Bucket, evt *event.Event, key uint64) error {
func put(streams, eventIDs *bolt.Bucket, evt *muksevt.Event, key uint64) error {
data, err := marshalEvent(evt)
if err != nil {
return err

View File

@ -36,15 +36,17 @@ import (
"github.com/pkg/errors"
"maunium.net/go/gomuks/lib/open"
"maunium.net/go/gomuks/matrix/event"
"maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix/pushrules"
)
// Container is a wrapper for a mautrix Client and some other stuff.
@ -96,7 +98,8 @@ func (c *Container) InitClient() error {
c.client = nil
}
var mxid, accessToken string
var mxid id.UserID
var accessToken string
if len(c.config.AccessToken) > 0 {
accessToken = c.config.AccessToken
mxid = c.config.UserID
@ -180,7 +183,7 @@ func respondHTML(w http.ResponseWriter, status int, message string) {
}
func (c *Container) SingleSignOn() error {
loginURL := c.client.BuildURLWithQuery([]string{"login", "sso", "redirect"}, map[string]string{
loginURL := c.client.BuildURLWithQuery(mautrix.URLPath{"login", "sso", "redirect"}, map[string]string{
"redirectUrl": "http://localhost:29325",
})
err := open.Open(loginURL)
@ -267,7 +270,7 @@ func (c *Container) Stop() {
// UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object.
func (c *Container) UpdatePushRules() {
debug.Print("Updating push rules...")
resp, err := pushrules.GetPushRules(c.client)
resp, err := c.client.GetPushRules()
if err != nil {
debug.Print("Failed to fetch push rules:", err)
c.config.PushRules = &pushrules.PushRuleset{}
@ -285,7 +288,10 @@ func (c *Container) PushRules() *pushrules.PushRuleset {
return c.config.PushRules
}
var AccountDataGomuksPreferences = mautrix.NewEventType("net.maunium.gomuks.preferences")
var AccountDataGomuksPreferences = event.Type{
Type: "net.maunium.gomuks.preferences",
Class: event.AccountDataEventType,
}
// OnLogin initializes the syncer and updates the room list.
func (c *Container) OnLogin() {
@ -295,21 +301,21 @@ func (c *Container) OnLogin() {
debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config)
c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventReaction, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction)
c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateRoomName, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership)
c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt)
c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping)
c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo)
c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules)
c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag)
c.syncer.OnEventType(event.EventMessage, c.HandleMessage)
c.syncer.OnEventType(event.EventEncrypted, c.HandleMessage)
c.syncer.OnEventType(event.EventSticker, c.HandleMessage)
c.syncer.OnEventType(event.EventReaction, c.HandleMessage)
c.syncer.OnEventType(event.EventRedaction, c.HandleRedaction)
c.syncer.OnEventType(event.StateAliases, c.HandleMessage)
c.syncer.OnEventType(event.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(event.StateTopic, c.HandleMessage)
c.syncer.OnEventType(event.StateRoomName, c.HandleMessage)
c.syncer.OnEventType(event.StateMember, c.HandleMembership)
c.syncer.OnEventType(event.EphemeralEventReceipt, c.HandleReadReceipt)
c.syncer.OnEventType(event.EphemeralEventTyping, c.HandleTyping)
c.syncer.OnEventType(event.AccountDataDirectChats, c.HandleDirectChatInfo)
c.syncer.OnEventType(event.AccountDataPushRules, c.HandlePushRules)
c.syncer.OnEventType(event.AccountDataRoomTags, c.HandleTag)
c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences)
c.syncer.InitDoneCallback = func() {
debug.Print("Initial sync done")
@ -372,7 +378,7 @@ func (c *Container) Start() {
}
}
func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) {
func (c *Container) HandlePreferences(source EventSource, evt *event.Event) {
if source&EventSourceAccountData == 0 {
return
}
@ -395,18 +401,17 @@ func (c *Container) Preferences() *config.UserPreferences {
func (c *Container) SendPreferencesToMatrix() {
defer debug.Recover()
debug.Print("Sending updated preferences:", c.config.Preferences)
u := c.client.BuildURL("user", c.config.UserID, "account_data", AccountDataGomuksPreferences.Type)
u := c.client.BuildURL("user", string(c.config.UserID), "account_data", AccountDataGomuksPreferences.Type)
_, err := c.client.MakeRequest("PUT", u, &c.config.Preferences, nil)
if err != nil {
debug.Print("Failed to update preferences:", err)
}
}
func (c *Container) HandleRedaction(source EventSource, evt *mautrix.Event) {
func (c *Container) HandleRedaction(source EventSource, evt *event.Event) {
room := c.GetOrCreateRoom(evt.RoomID)
var redactedEvt *event.Event
err := c.history.Update(room, evt.Redacts, func(redacted *event.Event) error {
redacted.Unsigned.RedactedBy = evt.ID
var redactedEvt *muksevt.Event
err := c.history.Update(room, evt.Redacts, func(redacted *muksevt.Event) error {
redacted.Unsigned.RedactedBecause = evt
redactedEvt = redacted
return nil
@ -430,9 +435,9 @@ func (c *Container) HandleRedaction(source EventSource, evt *mautrix.Event) {
}
}
func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *event.Event) {
var origEvt *event.Event
err := c.history.Update(room, editsID, func(evt *event.Event) error {
func (c *Container) HandleEdit(room *rooms.Room, editsID id.EventID, editEvent *muksevt.Event) {
var origEvt *muksevt.Event
err := c.history.Update(room, editsID, func(evt *muksevt.Event) error {
evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent)
origEvt = evt
return nil
@ -456,10 +461,10 @@ func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *even
}
}
func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent *event.Event) {
func (c *Container) HandleReaction(room *rooms.Room, reactsTo id.EventID, reactEvent *muksevt.Event) {
rel := reactEvent.Content.GetRelatesTo()
var origEvt *event.Event
err := c.history.Update(room, reactsTo, func(evt *event.Event) error {
var origEvt *muksevt.Event
err := c.history.Update(room, reactsTo, func(evt *muksevt.Event) error {
if evt.Unsigned.Relations.Annotations.Map == nil {
evt.Unsigned.Relations.Annotations.Map = make(map[string]int)
}
@ -488,7 +493,7 @@ func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent
}
// HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) {
func (c *Container) HandleMessage(source EventSource, mxEvent *event.Event) {
room := c.GetOrCreateRoom(mxEvent.RoomID)
if source&EventSourceLeave != 0 {
room.HasLeft = true
@ -498,14 +503,14 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) {
}
if editID := mxEvent.Content.GetRelatesTo().GetReplaceID(); len(editID) > 0 {
c.HandleEdit(room, editID, event.Wrap(mxEvent))
c.HandleEdit(room, editID, muksevt.Wrap(mxEvent))
return
} else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == mautrix.EventReaction && len(reactionID) > 0 {
c.HandleReaction(room, reactionID, event.Wrap(mxEvent))
} else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == event.EventReaction && len(reactionID) > 0 {
c.HandleReaction(room, reactionID, muksevt.Wrap(mxEvent))
return
}
events, err := c.history.Append(room, []*mautrix.Event{mxEvent})
events, err := c.history.Append(room, []*event.Event{mxEvent})
if err != nil {
debug.Printf("Failed to add event %s to history: %v", mxEvent.ID, err)
}
@ -549,7 +554,7 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) {
}
// HandleMembership is the event handler for the m.room.member state event.
func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
func (c *Container) HandleMembership(source EventSource, evt *event.Event) {
isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0
if isLeave {
@ -558,7 +563,7 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
isNonTimelineLeave := isLeave && !isTimeline
if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave {
return
} else if evt.StateKey != nil && *evt.StateKey == c.config.UserID {
} else if evt.StateKey != nil && id.UserID(*evt.StateKey) == c.config.UserID {
c.processOwnMembershipChange(evt)
} else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) {
// We don't care about other users' membership events in the initial sync or chats we've left.
@ -568,9 +573,9 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
c.HandleMessage(source, evt)
}
func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
func (c *Container) processOwnMembershipChange(evt *event.Event) {
membership := evt.Content.Membership
prevMembership := mautrix.MembershipLeave
prevMembership := event.MembershipLeave
if evt.Unsigned.PrevContent != nil {
prevMembership = evt.Unsigned.PrevContent.Membership
}
@ -603,7 +608,7 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
c.ui.Render()
}
func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent string) {
func (c *Container) parseReadReceipt(evt *event.Event) (largestTimestampEvent id.EventID) {
var largestTimestamp int64
for eventID, rawContent := range evt.Content.Raw {
content, ok := rawContent.(map[string]interface{})
@ -616,7 +621,7 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent
continue
}
myInfo, ok := mRead[c.config.UserID].(map[string]interface{})
myInfo, ok := mRead[string(c.config.UserID)].(map[string]interface{})
if !ok {
continue
}
@ -624,13 +629,13 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent
ts, ok := myInfo["ts"].(float64)
if int64(ts) > largestTimestamp {
largestTimestamp = int64(ts)
largestTimestampEvent = eventID
largestTimestampEvent = id.EventID(eventID)
}
}
return
}
func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) {
func (c *Container) HandleReadReceipt(source EventSource, evt *event.Event) {
if source&EventSourceLeave != 0 {
return
}
@ -649,7 +654,7 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) {
}
}
func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool {
func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]bool {
directChats := make(map[*rooms.Room]bool)
for _, rawRoomIDList := range evt.Content.Raw {
roomIDList, ok := rawRoomIDList.([]interface{})
@ -663,7 +668,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool
continue
}
room := c.GetOrCreateRoom(roomID)
room := c.GetOrCreateRoom(id.RoomID(roomID))
if room != nil && !room.HasLeft {
directChats[room] = true
}
@ -672,7 +677,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool
return directChats
}
func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) {
func (c *Container) HandleDirectChatInfo(_ EventSource, evt *event.Event) {
directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Rooms.Map {
shouldBeDirect := directChats[room]
@ -686,7 +691,7 @@ func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) {
}
// HandlePushRules is the event handler for the m.push_rules account data event.
func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) {
func (c *Container) HandlePushRules(_ EventSource, evt *event.Event) {
debug.Print("Received updated push rules")
var err error
c.config.PushRules, err = pushrules.EventToPushRules(evt)
@ -698,7 +703,7 @@ func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) {
}
// HandleTag is the event handler for the m.tag account data event.
func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) {
func (c *Container) HandleTag(_ EventSource, evt *event.Event) {
debug.Printf("Received tags for %s: %s -- %s", evt.RoomID, evt.Content.RoomTags, string(evt.Content.VeryRaw))
room := c.GetOrCreateRoom(evt.RoomID)
@ -724,24 +729,24 @@ func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) {
}
// HandleTyping is the event handler for the m.typing event.
func (c *Container) HandleTyping(_ EventSource, evt *mautrix.Event) {
func (c *Container) HandleTyping(_ EventSource, evt *event.Event) {
if !c.config.AuthCache.InitialSyncDone {
return
}
c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs)
}
func (c *Container) MarkRead(roomID, eventID string) {
func (c *Container) MarkRead(roomID id.RoomID, eventID id.EventID) {
urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID)
_, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil)
}
func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, rel *ifc.Relation) *event.Event {
var content mautrix.Content
func (c *Container) PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, rel *ifc.Relation) *muksevt.Event {
var content event.Content
if html != "" {
content = mautrix.Content{
content = event.Content{
FormattedBody: html,
Format: mautrix.FormatHTML,
Format: event.FormatHTML,
Body: text,
MsgType: msgtype,
}
@ -750,49 +755,49 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag
content.MsgType = msgtype
}
if rel != nil && rel.Type == mautrix.RelReplace {
if rel != nil && rel.Type == event.RelReplace {
contentCopy := content
content.NewContent = &contentCopy
content.Body = "* " + content.Body
if len(content.FormattedBody) > 0 {
content.FormattedBody = "* " + content.FormattedBody
}
content.RelatesTo = &mautrix.RelatesTo{
Type: mautrix.RelReplace,
content.RelatesTo = &event.RelatesTo{
Type: event.RelReplace,
EventID: rel.Event.ID,
}
} else if rel != nil && rel.Type == mautrix.RelReference {
} else if rel != nil && rel.Type == event.RelReference {
content.SetReply(rel.Event.Event)
}
txnID := c.client.TxnID()
localEcho := event.Wrap(&mautrix.Event{
ID: txnID,
localEcho := muksevt.Wrap(&event.Event{
ID: id.EventID(txnID),
Sender: c.config.UserID,
Type: mautrix.EventMessage,
Type: event.EventMessage,
Timestamp: time.Now().UnixNano() / 1e6,
RoomID: roomID,
Content: content,
Unsigned: mautrix.Unsigned{
Unsigned: event.Unsigned{
TransactionID: txnID,
},
})
localEcho.Gomuks.OutgoingState = event.StateLocalEcho
if rel != nil && rel.Type == mautrix.RelReplace {
localEcho.Gomuks.OutgoingState = muksevt.StateLocalEcho
if rel != nil && rel.Type == event.RelReplace {
localEcho.ID = rel.Event.ID
localEcho.Gomuks.Edits = []*event.Event{localEcho}
localEcho.Gomuks.Edits = []*muksevt.Event{localEcho}
}
return localEcho
}
func (c *Container) Redact(roomID, eventID, reason string) error {
func (c *Container) Redact(roomID id.RoomID, eventID id.EventID, reason string) error {
defer debug.Recover()
_, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason})
return err
}
// SendMessage sends the given event.
func (c *Container) SendEvent(event *event.Event) (string, error) {
func (c *Container) SendEvent(event *muksevt.Event) (id.EventID, error) {
defer debug.Recover()
c.client.UserTyping(event.RoomID, false, 0)
@ -804,13 +809,13 @@ func (c *Container) SendEvent(event *event.Event) (string, error) {
return resp.EventID, nil
}
func (c *Container) sendTypingAsync(roomID string, typing bool, timeout int64) {
func (c *Container) sendTypingAsync(roomID id.RoomID, typing bool, timeout int64) {
defer debug.Recover()
_, _ = c.client.UserTyping(roomID, typing, timeout)
}
// SendTyping sets whether or not the user is typing in the given room.
func (c *Container) SendTyping(roomID string, typing bool) {
func (c *Container) SendTyping(roomID id.RoomID, typing bool) {
ts := time.Now().Unix()
if (c.typing > ts && typing) || (c.typing == 0 && !typing) {
return
@ -836,8 +841,8 @@ func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error)
}
// JoinRoom makes the current user try to join the given room.
func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) {
resp, err := c.client.JoinRoom(roomID, server, nil)
func (c *Container) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) {
resp, err := c.client.JoinRoom(string(roomID), server, nil)
if err != nil {
return nil, err
}
@ -848,7 +853,7 @@ func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) {
}
// LeaveRoom makes the current user leave the given room.
func (c *Container) LeaveRoom(roomID string) error {
func (c *Container) LeaveRoom(roomID id.RoomID) error {
_, err := c.client.LeaveRoom(roomID)
if err != nil {
return err
@ -873,7 +878,7 @@ func (c *Container) FetchMembers(room *rooms.Room) error {
}
// GetHistory fetches room history.
func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, error) {
func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*muksevt.Event, error) {
events, err := c.history.Load(room, limit)
if err != nil {
return nil, err
@ -893,7 +898,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err
room.PrevBatch = resp.End
c.config.Rooms.Put(room)
if len(resp.Chunk) == 0 {
return []*event.Event{}, nil
return []*muksevt.Event{}, nil
}
events, err = c.history.Prepend(room, resp.Chunk)
if err != nil {
@ -902,7 +907,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err
return events, nil
}
func (c *Container) GetEvent(room *rooms.Room, eventID string) (*event.Event, error) {
func (c *Container) GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error) {
evt, err := c.history.Get(room, eventID)
if err != nil && err != EventNotFoundError {
debug.Printf("Failed to get event %s from local cache: %v", eventID, err)
@ -914,18 +919,18 @@ func (c *Container) GetEvent(room *rooms.Room, eventID string) (*event.Event, er
if err != nil {
return nil, err
}
evt = event.Wrap(mxEvent)
evt = muksevt.Wrap(mxEvent)
debug.Printf("Loaded event %s from server", eventID)
return evt, nil
}
// GetOrCreateRoom gets the room instance stored in the session.
func (c *Container) GetOrCreateRoom(roomID string) *rooms.Room {
func (c *Container) GetOrCreateRoom(roomID id.RoomID) *rooms.Room {
return c.config.Rooms.GetOrCreate(roomID)
}
// GetRoom gets the room instance stored in the session.
func (c *Container) GetRoom(roomID string) *rooms.Room {
func (c *Container) GetRoom(roomID id.RoomID) *rooms.Room {
return c.config.Rooms.Get(roomID)
}
@ -949,7 +954,7 @@ func cp(src, dst string) error {
return out.Close()
}
func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullPath string, err error) {
func (c *Container) DownloadToDisk(uri id.ContentURI, target string) (fullPath string, err error) {
cachePath := c.GetCachePath(uri)
if target == "" {
fullPath = cachePath
@ -994,7 +999,7 @@ func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullP
// Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors.
//
// The file will be either read from the media cache (if found) or downloaded from the server.
func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) {
func (c *Container) Download(uri id.ContentURI) (data []byte, err error) {
cacheFile := c.GetCachePath(uri)
var info os.FileInfo
if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() {
@ -1008,7 +1013,7 @@ func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) {
return
}
func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string {
func (c *Container) GetDownloadURL(uri id.ContentURI) string {
dlURL, _ := url.Parse(c.client.HomeserverURL.String())
if dlURL.Scheme == "" {
dlURL.Scheme = "https"
@ -1017,7 +1022,7 @@ func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string {
return dlURL.String()
}
func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []byte, err error) {
func (c *Container) download(uri id.ContentURI, cacheFile string) (data []byte, err error) {
var resp *http.Response
resp, err = c.client.Client.Get(c.GetDownloadURL(uri))
if err != nil {
@ -1039,7 +1044,7 @@ func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []b
// GetCachePath gets the path to the cached version of the given homeserver:fileID combination.
// The file may or may not exist, use Download() to ensure it has been cached.
func (c *Container) GetCachePath(uri mautrix.ContentURI) string {
func (c *Container) GetCachePath(uri id.ContentURI) string {
dir := filepath.Join(c.config.MediaDir, uri.Homeserver)
err := os.MkdirAll(dir, 0700)

View File

@ -1,232 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package matrix
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/gomuks/config"
"maunium.net/go/mautrix"
)
func TestContainer_InitClient_Empty(t *testing.T) {
defer os.RemoveAll("/tmp/gomuks-mxtest-0")
os.MkdirAll("/tmp/gomuks-mxtest-0", 0700)
cfg := config.NewConfig("/tmp/gomuks-mxtest-0", "/tmp/gomuks-mxtest-0")
cfg.HS = "https://matrix.org"
c := Container{config: cfg}
assert.Nil(t, c.InitClient())
}
func TestContainer_GetCachePath(t *testing.T) {
defer os.RemoveAll("/tmp/gomuks-mxtest-1")
cfg := config.NewConfig("/tmp/gomuks-mxtest-1", "/tmp/gomuks-mxtest-1")
c := Container{config: cfg}
assert.Equal(t, "/tmp/gomuks-mxtest-1/media/maunium.net/foobar", c.GetCachePath("maunium.net", "foobar"))
}
/* FIXME probably not applicable anymore
func TestContainer_SendMarkdownMessage_NoMarkdown(t *testing.T) {
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodPut || !strings.HasPrefix(req.URL.Path, "/_matrix/client/r0/rooms/!foo:example.com/send/m.room.message/") {
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
}
body := parseBody(req)
assert.Equal(t, "m.text", body["msgtype"])
assert.Equal(t, "test message", body["body"])
return mockResponse(http.StatusOK, `{"event_id": "!foobar1:example.com"}`), nil
})}
event := c.PrepareMarkdownMessage("!foo:example.com", "m.text", "test message")
evtID, err := c.SendEvent(event)
assert.Nil(t, err)
assert.Equal(t, "!foobar1:example.com", evtID)
}*/
func TestContainer_SendMarkdownMessage_WithMarkdown(t *testing.T) {
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodPut || !strings.HasPrefix(req.URL.Path, "/_matrix/client/r0/rooms/!foo:example.com/send/m.room.message/") {
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
}
body := parseBody(req)
assert.Equal(t, "m.text", body["msgtype"])
assert.Equal(t, "**formatted** test _message_", body["body"])
assert.Equal(t, "<p><strong>formatted</strong> <u>test</u> <em>message</em></p>\n", body["formatted_body"])
return mockResponse(http.StatusOK, `{"event_id": "!foobar2:example.com"}`), nil
}), config: &config.Config{UserID: "@user:example.com"}}
event := c.PrepareMarkdownMessage("!foo:example.com", "m.text", "**formatted** <u>test</u> _message_")
evtID, err := c.SendEvent(event)
assert.Nil(t, err)
assert.Equal(t, "!foobar2:example.com", evtID)
}
func TestContainer_SendTyping(t *testing.T) {
var calls []mautrix.ReqTyping
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodPut || req.URL.Path != "/_matrix/client/r0/rooms/!foo:example.com/typing/@user:example.com" {
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
}
rawBody, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
call := mautrix.ReqTyping{}
err = json.Unmarshal(rawBody, &call)
if err != nil {
return nil, err
}
calls = append(calls, call)
return mockResponse(http.StatusOK, `{}`), nil
})}
c.SendTyping("!foo:example.com", true)
c.SendTyping("!foo:example.com", true)
c.SendTyping("!foo:example.com", true)
c.SendTyping("!foo:example.com", false)
c.SendTyping("!foo:example.com", true)
c.SendTyping("!foo:example.com", false)
assert.Len(t, calls, 4)
assert.True(t, calls[0].Typing)
assert.False(t, calls[1].Typing)
assert.True(t, calls[2].Typing)
assert.False(t, calls[3].Typing)
}
func TestContainer_JoinRoom(t *testing.T) {
defer os.RemoveAll("/tmp/gomuks-mxtest-2")
cfg := config.NewConfig("/tmp/gomuks-mxtest-2", "/tmp/gomuks-mxtest-2")
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method == http.MethodPost && req.URL.Path == "/_matrix/client/r0/join/!foo:example.com" {
return mockResponse(http.StatusOK, `{"room_id": "!foo:example.com"}`), nil
} else if req.Method == http.MethodPost && req.URL.Path == "/_matrix/client/r0/rooms/!foo:example.com/leave" {
return mockResponse(http.StatusOK, `{}`), nil
}
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
}), config: cfg}
room, err := c.JoinRoom("!foo:example.com", "")
assert.Nil(t, err)
assert.Equal(t, "!foo:example.com", room.ID)
assert.False(t, room.HasLeft)
err = c.LeaveRoom("!foo:example.com")
assert.Nil(t, err)
assert.True(t, room.HasLeft)
}
func TestContainer_Download(t *testing.T) {
defer os.RemoveAll("/tmp/gomuks-mxtest-3")
cfg := config.NewConfig("/tmp/gomuks-mxtest-3", "/tmp/gomuks-mxtest-3")
cfg.LoadAll()
callCounter := 0
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodGet || req.URL.Path != "/_matrix/media/v1/download/example.com/foobar" {
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
}
callCounter++
return mockResponse(http.StatusOK, `example file`), nil
}), config: cfg}
// Check that download works
data, hs, id, err := c.Download("mxc://example.com/foobar")
assert.Equal(t, "example.com", hs)
assert.Equal(t, "foobar", id)
assert.Equal(t, 1, callCounter)
assert.Equal(t, []byte("example file"), data)
assert.Nil(t, err)
// Check that cache works
data, _, _, err = c.Download("mxc://example.com/foobar")
assert.Nil(t, err)
assert.Equal(t, []byte("example file"), data)
assert.Equal(t, 1, callCounter)
}
func TestContainer_Download_InvalidURL(t *testing.T) {
c := Container{}
data, hs, id, err := c.Download("mxc://invalid mxc")
assert.NotNil(t, err)
assert.Empty(t, id)
assert.Empty(t, hs)
assert.Empty(t, data)
}
/* FIXME
func TestContainer_GetHistory(t *testing.T) {
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodGet || req.URL.Path != "/_matrix/client/r0/rooms/!foo:maunium.net/messages" {
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
}
return mockResponse(http.StatusOK, `{"start": "123", "end": "456", "chunk": [{"event_id": "it works"}]}`), nil
})}
history, prevBatch, err := c.GetHistory("!foo:maunium.net", "123", 5)
assert.Nil(t, err)
assert.Equal(t, "it works", history[0].ID)
assert.Equal(t, "456", prevBatch)
}*/
func mockClient(fn func(*http.Request) (*http.Response, error)) *mautrix.Client {
client, _ := mautrix.NewClient("https://example.com", "@user:example.com", "foobar")
client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}}
return client
}
func parseBody(req *http.Request) map[string]interface{} {
rawBody, err := ioutil.ReadAll(req.Body)
if err != nil {
panic(err)
}
data := make(map[string]interface{})
err = json.Unmarshal(rawBody, &data)
if err != nil {
panic(err)
}
return data
}
func mockResponse(status int, body string) *http.Response {
return &http.Response{
StatusCode: status,
Body: ioutil.NopCloser(strings.NewReader(body)),
}
}
type MockRoundTripper struct {
RT func(*http.Request) (*http.Response, error)
}
func (t MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return t.RT(req)
}

View File

@ -14,14 +14,14 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package event
package muksevt
import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
type Event struct {
*mautrix.Event
*event.Event
Gomuks GomuksContent `json:"-"`
}
@ -33,7 +33,7 @@ func (evt *Event) SomewhatDangerousCopy() *Event {
}
}
func Wrap(event *mautrix.Event) *Event {
func Wrap(event *event.Event) *Event {
return &Event{Event: event}
}

View File

@ -1,134 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules
import "encoding/json"
// PushActionType is the type of a PushAction
type PushActionType string
// The allowed push action types as specified in spec section 11.12.1.4.1.
const (
ActionNotify PushActionType = "notify"
ActionDontNotify PushActionType = "dont_notify"
ActionCoalesce PushActionType = "coalesce"
ActionSetTweak PushActionType = "set_tweak"
)
// PushActionTweak is the type of the tweak in SetTweak push actions.
type PushActionTweak string
// The allowed tweak types as specified in spec section 11.12.1.4.1.1.
const (
TweakSound PushActionTweak = "sound"
TweakHighlight PushActionTweak = "highlight"
)
// PushActionArray is an array of PushActions.
type PushActionArray []*PushAction
// PushActionArrayShould contains the important information parsed from a PushActionArray.
type PushActionArrayShould struct {
// Whether or not the array contained a Notify, DontNotify or Coalesce action type.
NotifySpecified bool
// Whether or not the event in question should trigger a notification.
Notify bool
// Whether or not the event in question should be highlighted.
Highlight bool
// Whether or not the event in question should trigger a sound alert.
PlaySound bool
// The name of the sound to play if PlaySound is true.
SoundName string
}
// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct.
func (actions PushActionArray) Should() (should PushActionArrayShould) {
for _, action := range actions {
switch action.Action {
case ActionNotify, ActionCoalesce:
should.Notify = true
should.NotifySpecified = true
case ActionDontNotify:
should.Notify = false
should.NotifySpecified = true
case ActionSetTweak:
switch action.Tweak {
case TweakHighlight:
var ok bool
should.Highlight, ok = action.Value.(bool)
if !ok {
// Highlight value not specified, so assume true since the tweak is set.
should.Highlight = true
}
case TweakSound:
should.SoundName = action.Value.(string)
should.PlaySound = len(should.SoundName) > 0
}
}
}
return
}
// PushAction is a single action that should be triggered when receiving a message.
type PushAction struct {
Action PushActionType
Tweak PushActionTweak
Value interface{}
}
// UnmarshalJSON parses JSON into this PushAction.
//
// * If the JSON is a single string, the value is stored in the Action field.
// * If the JSON is an object with the set_tweak field, Action will be set to
// "set_tweak", Tweak will be set to the value of the set_tweak field and
// and Value will be set to the value of the value field.
// * In any other case, the function does nothing.
func (action *PushAction) UnmarshalJSON(raw []byte) error {
var data interface{}
err := json.Unmarshal(raw, &data)
if err != nil {
return err
}
switch val := data.(type) {
case string:
action.Action = PushActionType(val)
case map[string]interface{}:
tweak, ok := val["set_tweak"].(string)
if ok {
action.Action = ActionSetTweak
action.Tweak = PushActionTweak(tweak)
action.Value, _ = val["value"]
}
}
return nil
}
// MarshalJSON is the reverse of UnmarshalJSON()
func (action *PushAction) MarshalJSON() (raw []byte, err error) {
if action.Action == ActionSetTweak {
data := map[string]interface{}{
"set_tweak": action.Tweak,
"value": action.Value,
}
return json.Marshal(&data)
}
data := string(action.Action)
return json.Marshal(&data)
}

View File

@ -1,210 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/gomuks/matrix/pushrules"
)
func TestPushActionArray_Should_EmptyArrayReturnsDefaults(t *testing.T) {
should := pushrules.PushActionArray{}.Should()
assert.False(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.False(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushActionArray_Should_MixedArrayReturnsExpected1(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"},
}.Should()
assert.True(t, should.NotifySpecified)
assert.True(t, should.Notify)
assert.True(t, should.Highlight)
assert.True(t, should.PlaySound)
assert.Equal(t, "ping", should.SoundName)
}
func TestPushActionArray_Should_MixedArrayReturnsExpected2(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: ""},
}.Should()
assert.True(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.False(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushActionArray_Should_NotifySet(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
}.Should()
assert.True(t, should.NotifySpecified)
assert.True(t, should.Notify)
assert.False(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushActionArray_Should_NotifyAndCoalesceDoTheSameThing(t *testing.T) {
should1 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
}.Should()
should2 := pushrules.PushActionArray{
{Action: pushrules.ActionCoalesce},
}.Should()
assert.Equal(t, should1, should2)
}
func TestPushActionArray_Should_DontNotify(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
}.Should()
assert.True(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.False(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushActionArray_Should_HighlightBlank(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight},
}.Should()
assert.False(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.True(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushActionArray_Should_HighlightFalse(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
}.Should()
assert.False(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.False(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushActionArray_Should_SoundName(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"},
}.Should()
assert.False(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.False(t, should.Highlight)
assert.True(t, should.PlaySound)
assert.Equal(t, "ping", should.SoundName)
}
func TestPushActionArray_Should_SoundNameEmpty(t *testing.T) {
should := pushrules.PushActionArray{
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: ""},
}.Should()
assert.False(t, should.NotifySpecified)
assert.False(t, should.Notify)
assert.False(t, should.Highlight)
assert.False(t, should.PlaySound)
assert.Empty(t, should.SoundName)
}
func TestPushAction_UnmarshalJSON_InvalidJSONFails(t *testing.T) {
pa := &pushrules.PushAction{}
err := pa.UnmarshalJSON([]byte("Not JSON"))
assert.NotNil(t, err)
}
func TestPushAction_UnmarshalJSON_InvalidTypeDoesNothing(t *testing.T) {
pa := &pushrules.PushAction{
Action: pushrules.PushActionType("unchanged"),
Tweak: pushrules.PushActionTweak("unchanged"),
Value: "unchanged",
}
err := pa.UnmarshalJSON([]byte(`{"foo": "bar"}`))
assert.Nil(t, err)
err = pa.UnmarshalJSON([]byte(`9001`))
assert.Nil(t, err)
assert.Equal(t, pushrules.PushActionType("unchanged"), pa.Action)
assert.Equal(t, pushrules.PushActionTweak("unchanged"), pa.Tweak)
assert.Equal(t, "unchanged", pa.Value)
}
func TestPushAction_UnmarshalJSON_StringChangesActionType(t *testing.T) {
pa := &pushrules.PushAction{
Action: pushrules.PushActionType("unchanged"),
Tweak: pushrules.PushActionTweak("unchanged"),
Value: "unchanged",
}
err := pa.UnmarshalJSON([]byte(`"foo"`))
assert.Nil(t, err)
assert.Equal(t, pushrules.PushActionType("foo"), pa.Action)
assert.Equal(t, pushrules.PushActionTweak("unchanged"), pa.Tweak)
assert.Equal(t, "unchanged", pa.Value)
}
func TestPushAction_UnmarshalJSON_SetTweakChangesTweak(t *testing.T) {
pa := &pushrules.PushAction{
Action: pushrules.PushActionType("unchanged"),
Tweak: pushrules.PushActionTweak("unchanged"),
Value: "unchanged",
}
err := pa.UnmarshalJSON([]byte(`{"set_tweak": "foo", "value": 123.0}`))
assert.Nil(t, err)
assert.Equal(t, pushrules.ActionSetTweak, pa.Action)
assert.Equal(t, pushrules.PushActionTweak("foo"), pa.Tweak)
assert.Equal(t, 123.0, pa.Value)
}
func TestPushAction_MarshalJSON_TweakOutputWorks(t *testing.T) {
pa := &pushrules.PushAction{
Action: pushrules.ActionSetTweak,
Tweak: pushrules.PushActionTweak("foo"),
Value: "bar",
}
data, err := pa.MarshalJSON()
assert.Nil(t, err)
assert.Equal(t, []byte(`{"set_tweak":"foo","value":"bar"}`), data)
}
func TestPushAction_MarshalJSON_OtherOutputWorks(t *testing.T) {
pa := &pushrules.PushAction{
Action: pushrules.PushActionType("something else"),
Tweak: pushrules.PushActionTweak("foo"),
Value: "bar",
}
data, err := pa.MarshalJSON()
assert.Nil(t, err)
assert.Equal(t, []byte(`"something else"`), data)
}

View File

@ -1,162 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules
import (
"regexp"
"strconv"
"strings"
"unicode"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix"
"maunium.net/go/gomuks/lib/glob"
)
// Room is an interface with the functions that are needed for processing room-specific push conditions
type Room interface {
GetMember(mxid string) *rooms.Member
GetMembers() map[string]*rooms.Member
GetSessionOwner() string
}
// PushCondKind is the type of a push condition.
type PushCondKind string
// The allowed push condition kinds as specified in section 11.12.1.4.3 of r0.3.0 of the Client-Server API.
const (
KindEventMatch PushCondKind = "event_match"
KindContainsDisplayName PushCondKind = "contains_display_name"
KindRoomMemberCount PushCondKind = "room_member_count"
)
// PushCondition wraps a condition that is required for a specific PushRule to be used.
type PushCondition struct {
// The type of the condition.
Kind PushCondKind `json:"kind"`
// The dot-separated field of the event to match. Only applicable if kind is EventMatch.
Key string `json:"key,omitempty"`
// The glob-style pattern to match the field against. Only applicable if kind is EventMatch.
Pattern string `json:"pattern,omitempty"`
// The condition that needs to be fulfilled for RoomMemberCount-type conditions.
// A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found.
MemberCountCondition string `json:"is,omitempty"`
}
// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions.
var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$")
// Match checks if this condition is fulfilled for the given event in the given room.
func (cond *PushCondition) Match(room Room, event *mautrix.Event) bool {
switch cond.Kind {
case KindEventMatch:
return cond.matchValue(room, event)
case KindContainsDisplayName:
return cond.matchDisplayName(room, event)
case KindRoomMemberCount:
return cond.matchMemberCount(room, event)
default:
return false
}
}
func (cond *PushCondition) matchValue(room Room, event *mautrix.Event) bool {
index := strings.IndexRune(cond.Key, '.')
key := cond.Key
subkey := ""
if index > 0 {
subkey = key[index+1:]
key = key[0:index]
}
pattern, err := glob.Compile(cond.Pattern)
if err != nil {
return false
}
switch key {
case "type":
return pattern.MatchString(event.Type.String())
case "sender":
return pattern.MatchString(event.Sender)
case "room_id":
return pattern.MatchString(event.RoomID)
case "state_key":
if event.StateKey == nil {
return cond.Pattern == ""
}
return pattern.MatchString(*event.StateKey)
case "content":
val, _ := event.Content.Raw[subkey].(string)
return pattern.MatchString(val)
default:
return false
}
}
func (cond *PushCondition) matchDisplayName(room Room, event *mautrix.Event) bool {
ownerID := room.GetSessionOwner()
if ownerID == event.Sender {
return false
}
member := room.GetMember(ownerID)
if member == nil {
return false
}
msg := event.Content.Body
isAcceptable := func(r uint8) bool {
return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r))
}
length := len(member.Displayname)
for index := strings.Index(msg, member.Displayname); index != -1; index = strings.Index(msg, member.Displayname) {
if (index <= 0 || isAcceptable(msg[index-1])) && (index + length >= len(msg) || isAcceptable(msg[index+length])) {
return true
}
msg = msg[index+len(member.Displayname):]
}
return false
}
func (cond *PushCondition) matchMemberCount(room Room, event *mautrix.Event) bool {
group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition)
if len(group) != 3 {
return false
}
operator := group[1]
wantedMemberCount, _ := strconv.Atoi(group[2])
memberCount := len(room.GetMembers())
switch operator {
case "==", "":
return memberCount == wantedMemberCount
case ">":
return memberCount > wantedMemberCount
case ">=":
return memberCount >= wantedMemberCount
case "<":
return memberCount < wantedMemberCount
case "<=":
return memberCount <= wantedMemberCount
default:
// Should be impossible due to regex.
return false
}
}

View File

@ -1,60 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"maunium.net/go/mautrix"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPushCondition_Match_DisplayName(t *testing.T) {
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgText,
Body: "tulir: test mention",
})
event.Sender = "@someone_else:matrix.org"
assert.True(t, displaynamePushCondition.Match(displaynameTestRoom, event))
}
func TestPushCondition_Match_DisplayName_Fail(t *testing.T) {
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgText,
Body: "not a mention",
})
event.Sender = "@someone_else:matrix.org"
assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event))
}
func TestPushCondition_Match_DisplayName_CantHighlightSelf(t *testing.T) {
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgText,
Body: "tulir: I can't highlight myself",
})
assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event))
}
func TestPushCondition_Match_DisplayName_FailsOnEmptyRoom(t *testing.T) {
emptyRoom := newFakeRoom(0)
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgText,
Body: "tulir: this room doesn't have the owner Member available, so it fails.",
})
event.Sender = "@someone_else:matrix.org"
assert.False(t, displaynamePushCondition.Match(emptyRoom, event))
}

View File

@ -1,96 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"maunium.net/go/mautrix"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPushCondition_Match_KindEvent_MsgType(t *testing.T) {
condition := newMatchPushCondition("content.msgtype", "m.emote")
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
Raw: map[string]interface{}{
"msgtype": "m.emote",
"body": "tests gomuks pushconditions",
},
})
assert.True(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_MsgType_Fail(t *testing.T) {
condition := newMatchPushCondition("content.msgtype", "m.emote")
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
Raw: map[string]interface{}{
"msgtype": "m.text",
"body": "I'm testing gomuks pushconditions",
},
})
assert.False(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_EventType(t *testing.T) {
condition := newMatchPushCondition("type", "m.room.foo")
event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{})
assert.True(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_EventType_IllegalGlob(t *testing.T) {
condition := newMatchPushCondition("type", "m.room.invalid_glo[b")
event := newFakeEvent(mautrix.NewEventType("m.room.invalid_glob"), mautrix.Content{})
assert.False(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_Sender_Fail(t *testing.T) {
condition := newMatchPushCondition("sender", "@foo:maunium.net")
event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{})
assert.False(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_RoomID(t *testing.T) {
condition := newMatchPushCondition("room_id", "!fakeroom:maunium.net")
event := newFakeEvent(mautrix.NewEventType(""), mautrix.Content{})
assert.True(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_BlankStateKey(t *testing.T) {
condition := newMatchPushCondition("state_key", "")
event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{})
assert.True(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_BlankStateKey_Fail(t *testing.T) {
condition := newMatchPushCondition("state_key", "not blank")
event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{})
assert.False(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_NonBlankStateKey(t *testing.T) {
condition := newMatchPushCondition("state_key", "*:maunium.net")
event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{})
event.StateKey = &event.Sender
assert.True(t, condition.Match(blankTestRoom, event))
}
func TestPushCondition_Match_KindEvent_UnknownKey(t *testing.T) {
condition := newMatchPushCondition("non-existent key", "doesn't affect anything")
event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{})
assert.False(t, condition.Match(blankTestRoom, event))
}

View File

@ -1,71 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPushCondition_Match_KindMemberCount_OneToOne_ImplicitPrefix(t *testing.T) {
condition := newCountPushCondition("2")
room := newFakeRoom(2)
assert.True(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_OneToOne_ExplicitPrefix(t *testing.T) {
condition := newCountPushCondition("==2")
room := newFakeRoom(2)
assert.True(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_BigRoom(t *testing.T) {
condition := newCountPushCondition(">200")
room := newFakeRoom(201)
assert.True(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_BigRoom_Fail(t *testing.T) {
condition := newCountPushCondition(">=200")
room := newFakeRoom(199)
assert.False(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_SmallRoom(t *testing.T) {
condition := newCountPushCondition("<10")
room := newFakeRoom(9)
assert.True(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_SmallRoom_Fail(t *testing.T) {
condition := newCountPushCondition("<=10")
room := newFakeRoom(11)
assert.False(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_InvalidPrefix(t *testing.T) {
condition := newCountPushCondition("??10")
room := newFakeRoom(11)
assert.False(t, condition.Match(room, countConditionTestEvent))
}
func TestPushCondition_Match_KindMemberCount_InvalidCondition(t *testing.T) {
condition := newCountPushCondition("foobar")
room := newFakeRoom(1)
assert.False(t, condition.Match(room, countConditionTestEvent))
}

View File

@ -1,132 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
)
var (
blankTestRoom *rooms.Room
displaynameTestRoom pushrules.Room
countConditionTestEvent *mautrix.Event
displaynamePushCondition *pushrules.PushCondition
)
func init() {
blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net")
countConditionTestEvent = &mautrix.Event{
Sender: "@tulir:maunium.net",
Type: mautrix.EventMessage,
Timestamp: 1523791120,
ID: "$123:maunium.net",
RoomID: "!fakeroom:maunium.net",
Content: mautrix.Content{
MsgType: mautrix.MsgText,
Body: "test",
},
}
displaynameTestRoom = newFakeRoom(4)
displaynamePushCondition = &pushrules.PushCondition{
Kind: pushrules.KindContainsDisplayName,
}
}
func newFakeEvent(evtType mautrix.EventType, content mautrix.Content) *mautrix.Event {
return &mautrix.Event{
Sender: "@tulir:maunium.net",
Type: evtType,
Timestamp: 1523791120,
ID: "$123:maunium.net",
RoomID: "!fakeroom:maunium.net",
Content: content,
}
}
func newCountPushCondition(condition string) *pushrules.PushCondition {
return &pushrules.PushCondition{
Kind: pushrules.KindRoomMemberCount,
MemberCountCondition: condition,
}
}
func newMatchPushCondition(key, pattern string) *pushrules.PushCondition {
return &pushrules.PushCondition{
Kind: pushrules.KindEventMatch,
Key: key,
Pattern: pattern,
}
}
func TestPushCondition_Match_InvalidKind(t *testing.T) {
condition := &pushrules.PushCondition{
Kind: pushrules.PushCondKind("invalid"),
}
event := newFakeEvent(mautrix.EventType{Type: "m.room.foobar"}, mautrix.Content{})
assert.False(t, condition.Match(blankTestRoom, event))
}
type FakeRoom struct {
members map[string]*mautrix.Member
owner string
}
func newFakeRoom(memberCount int) *FakeRoom {
room := &FakeRoom{
owner: "@tulir:maunium.net",
members: make(map[string]*mautrix.Member),
}
if memberCount >= 1 {
room.members["@tulir:maunium.net"] = &mautrix.Member{
Membership: mautrix.MembershipJoin,
Displayname: "tulir",
}
}
for i := 0; i < memberCount-1; i++ {
mxid := fmt.Sprintf("@extrauser_%d:matrix.org", i)
room.members[mxid] = &mautrix.Member{
Membership: mautrix.MembershipJoin,
Displayname: fmt.Sprintf("Extra User %d", i),
}
}
return room
}
func (fr *FakeRoom) GetMember(mxid string) *mautrix.Member {
return fr.members[mxid]
}
func (fr *FakeRoom) GetSessionOwner() string {
return fr.owner
}
func (fr *FakeRoom) GetMembers() map[string]*mautrix.Member {
return fr.members
}

View File

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

View File

@ -1,37 +0,0 @@
package pushrules
import (
"encoding/json"
"net/url"
"maunium.net/go/mautrix"
)
// GetPushRules returns the push notification rules for the global scope.
func GetPushRules(client *mautrix.Client) (*PushRuleset, error) {
return GetScopedPushRules(client, "global")
}
// GetScopedPushRules returns the push notification rules for the given scope.
func GetScopedPushRules(client *mautrix.Client, scope string) (resp *PushRuleset, err error) {
u, _ := url.Parse(client.BuildURL("pushrules", scope))
// client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash.
u.Path += "/"
_, err = client.MakeRequest("GET", u.String(), nil, &resp)
return
}
type contentWithRuleset struct {
Ruleset *PushRuleset `json:"global"`
}
// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON.
func EventToPushRules(event *mautrix.Event) (*PushRuleset, error) {
content := &contentWithRuleset{}
err := json.Unmarshal(event.Content.VeryRaw, content)
if err != nil {
return nil, err
}
return content.Ruleset, nil
}

View File

@ -1,249 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/pushrules"
)
func TestEventToPushRules(t *testing.T) {
event := &mautrix.Event{
Type: mautrix.AccountDataPushRules,
Timestamp: 1523380910,
Content: mautrix.Content{
VeryRaw: json.RawMessage(JSONExamplePushRules),
},
}
pushRuleset, err := pushrules.EventToPushRules(event)
assert.Nil(t, err)
assert.NotNil(t, pushRuleset)
assert.IsType(t, pushRuleset.Override, pushrules.PushRuleArray{})
assert.IsType(t, pushRuleset.Content, pushrules.PushRuleArray{})
assert.IsType(t, pushRuleset.Room, pushrules.PushRuleMap{})
assert.IsType(t, pushRuleset.Sender, pushrules.PushRuleMap{})
assert.IsType(t, pushRuleset.Underride, pushrules.PushRuleArray{})
assert.Len(t, pushRuleset.Override, 2)
assert.Len(t, pushRuleset.Content, 1)
assert.Empty(t, pushRuleset.Room.Map)
assert.Empty(t, pushRuleset.Sender.Map)
assert.Len(t, pushRuleset.Underride, 6)
assert.Len(t, pushRuleset.Content[0].Actions, 3)
assert.True(t, pushRuleset.Content[0].Default)
assert.True(t, pushRuleset.Content[0].Enabled)
assert.Empty(t, pushRuleset.Content[0].Conditions)
assert.Equal(t, "alice", pushRuleset.Content[0].Pattern)
assert.Equal(t, ".m.rule.contains_user_name", pushRuleset.Content[0].RuleID)
assert.False(t, pushRuleset.Override[0].Actions.Should().Notify)
assert.True(t, pushRuleset.Override[0].Actions.Should().NotifySpecified)
}
const JSONExamplePushRules = `{
"global": {
"content": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"default": true,
"enabled": true,
"pattern": "alice",
"rule_id": ".m.rule.contains_user_name"
}
],
"override": [
{
"actions": [
"dont_notify"
],
"conditions": [],
"default": true,
"enabled": false,
"rule_id": ".m.rule.master"
},
{
"actions": [
"dont_notify"
],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
}
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "ring"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.call.invite"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.call"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"conditions": [
{
"kind": "contains_display_name"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"is": "2",
"kind": "room_member_count"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
},
{
"key": "content.membership",
"kind": "event_match",
"pattern": "invite"
},
{
"key": "state_key",
"kind": "event_match",
"pattern": "@alice:example.com"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.invite_for_me"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.member_event"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.message"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.message"
}
]
}
}`

View File

@ -1,160 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules
import (
"encoding/gob"
"maunium.net/go/mautrix"
"maunium.net/go/gomuks/lib/glob"
)
func init() {
gob.Register(PushRuleArray{})
gob.Register(PushRuleMap{})
}
type PushRuleCollection interface {
GetActions(room Room, event *mautrix.Event) PushActionArray
}
type PushRuleArray []*PushRule
func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray {
for _, rule := range rules {
rule.Type = typ
}
return rules
}
func (rules PushRuleArray) GetActions(room Room, event *mautrix.Event) PushActionArray {
for _, rule := range rules {
if !rule.Match(room, event) {
continue
}
return rule.Actions
}
return nil
}
type PushRuleMap struct {
Map map[string]*PushRule
Type PushRuleType
}
func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap {
data := PushRuleMap{
Map: make(map[string]*PushRule),
Type: typ,
}
for _, rule := range rules {
rule.Type = typ
data.Map[rule.RuleID] = rule
}
return data
}
func (ruleMap PushRuleMap) GetActions(room Room, event *mautrix.Event) PushActionArray {
var rule *PushRule
var found bool
switch ruleMap.Type {
case RoomRule:
rule, found = ruleMap.Map[event.RoomID]
case SenderRule:
rule, found = ruleMap.Map[event.Sender]
}
if found && rule.Match(room, event) {
return rule.Actions
}
return nil
}
func (ruleMap PushRuleMap) Unmap() PushRuleArray {
array := make(PushRuleArray, len(ruleMap.Map))
index := 0
for _, rule := range ruleMap.Map {
array[index] = rule
index++
}
return array
}
type PushRuleType string
const (
OverrideRule PushRuleType = "override"
ContentRule PushRuleType = "content"
RoomRule PushRuleType = "room"
SenderRule PushRuleType = "sender"
UnderrideRule PushRuleType = "underride"
)
type PushRule struct {
// The type of this rule.
Type PushRuleType `json:"-"`
// The ID of this rule.
// For room-specific rules and user-specific rules, this is the room or user ID (respectively)
// For other types of rules, this doesn't affect anything.
RuleID string `json:"rule_id"`
// The actions this rule should trigger when matched.
Actions PushActionArray `json:"actions"`
// Whether this is a default rule, or has been set explicitly.
Default bool `json:"default"`
// Whether or not this push rule is enabled.
Enabled bool `json:"enabled"`
// The conditions to match in order to trigger this rule.
// Only applicable to generic underride/override rules.
Conditions []*PushCondition `json:"conditions,omitempty"`
// Pattern for content-specific push rules
Pattern string `json:"pattern,omitempty"`
}
func (rule *PushRule) Match(room Room, event *mautrix.Event) bool {
if !rule.Enabled {
return false
}
switch rule.Type {
case OverrideRule, UnderrideRule:
return rule.matchConditions(room, event)
case ContentRule:
return rule.matchPattern(room, event)
case RoomRule:
return rule.RuleID == event.RoomID
case SenderRule:
return rule.RuleID == event.Sender
default:
return false
}
}
func (rule *PushRule) matchConditions(room Room, event *mautrix.Event) bool {
for _, cond := range rule.Conditions {
if !cond.Match(room, event) {
return false
}
}
return true
}
func (rule *PushRule) matchPattern(room Room, event *mautrix.Event) bool {
pattern, err := glob.Compile(rule.Pattern)
if err != nil {
return false
}
return pattern.MatchString(event.Content.Body)
}

View File

@ -1,294 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"github.com/stretchr/testify/assert"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/mautrix"
"testing"
)
func TestPushRuleArray_GetActions_FirstMatchReturns(t *testing.T) {
cond1 := newMatchPushCondition("content.msgtype", "m.emote")
cond2 := newMatchPushCondition("content.body", "no match")
actions1 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"},
}
rule1 := &pushrules.PushRule{
Type: pushrules.OverrideRule,
Enabled: true,
Conditions: []*pushrules.PushCondition{cond1, cond2},
Actions: actions1,
}
actions2 := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"},
}
rule2 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!fakeroom:maunium.net",
Actions: actions2,
}
actions3 := pushrules.PushActionArray{
{Action: pushrules.ActionCoalesce},
}
rule3 := &pushrules.PushRule{
Type: pushrules.SenderRule,
Enabled: true,
RuleID: "@tulir:maunium.net",
Actions: actions3,
}
rules := pushrules.PushRuleArray{rule1, rule2, rule3}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.Equal(t, rules.GetActions(blankTestRoom, event), actions2)
}
func TestPushRuleArray_GetActions_NoMatchesIsNil(t *testing.T) {
cond1 := newMatchPushCondition("content.msgtype", "m.emote")
cond2 := newMatchPushCondition("content.body", "no match")
actions1 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"},
}
rule1 := &pushrules.PushRule{
Type: pushrules.OverrideRule,
Enabled: true,
Conditions: []*pushrules.PushCondition{cond1, cond2},
Actions: actions1,
}
actions2 := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"},
}
rule2 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!realroom:maunium.net",
Actions: actions2,
}
actions3 := pushrules.PushActionArray{
{Action: pushrules.ActionCoalesce},
}
rule3 := &pushrules.PushRule{
Type: pushrules.SenderRule,
Enabled: true,
RuleID: "@otheruser:maunium.net",
Actions: actions3,
}
rules := pushrules.PushRuleArray{rule1, rule2, rule3}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.Nil(t, rules.GetActions(blankTestRoom, event))
}
func TestPushRuleMap_GetActions_RoomRuleExists(t *testing.T) {
actions1 := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"},
}
rule1 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!realroom:maunium.net",
Actions: actions1,
}
actions2 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
}
rule2 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!thirdroom:maunium.net",
Actions: actions2,
}
actions3 := pushrules.PushActionArray{
{Action: pushrules.ActionCoalesce},
}
rule3 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!fakeroom:maunium.net",
Actions: actions3,
}
rules := pushrules.PushRuleMap{
Map: map[string]*pushrules.PushRule{
rule1.RuleID: rule1,
rule2.RuleID: rule2,
rule3.RuleID: rule3,
},
Type: pushrules.RoomRule,
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.Equal(t, rules.GetActions(blankTestRoom, event), actions3)
}
func TestPushRuleMap_GetActions_RoomRuleDoesntExist(t *testing.T) {
actions1 := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"},
}
rule1 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!realroom:maunium.net",
Actions: actions1,
}
actions2 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
}
rule2 := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!thirdroom:maunium.net",
Actions: actions2,
}
rules := pushrules.PushRuleMap{
Map: map[string]*pushrules.PushRule{
rule1.RuleID: rule1,
rule2.RuleID: rule2,
},
Type: pushrules.RoomRule,
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.Nil(t, rules.GetActions(blankTestRoom, event))
}
func TestPushRuleMap_GetActions_SenderRuleExists(t *testing.T) {
actions1 := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"},
}
rule1 := &pushrules.PushRule{
Type: pushrules.SenderRule,
Enabled: true,
RuleID: "@tulir:maunium.net",
Actions: actions1,
}
actions2 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
}
rule2 := &pushrules.PushRule{
Type: pushrules.SenderRule,
Enabled: true,
RuleID: "@someone:maunium.net",
Actions: actions2,
}
actions3 := pushrules.PushActionArray{
{Action: pushrules.ActionCoalesce},
}
rule3 := &pushrules.PushRule{
Type: pushrules.SenderRule,
Enabled: true,
RuleID: "@otheruser:matrix.org",
Actions: actions3,
}
rules := pushrules.PushRuleMap{
Map: map[string]*pushrules.PushRule{
rule1.RuleID: rule1,
rule2.RuleID: rule2,
rule3.RuleID: rule3,
},
Type: pushrules.SenderRule,
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.Equal(t, rules.GetActions(blankTestRoom, event), actions1)
}
func TestPushRuleArray_SetTypeAndMap(t *testing.T) {
actions1 := pushrules.PushActionArray{
{Action: pushrules.ActionDontNotify},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false},
{Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"},
}
rule1 := &pushrules.PushRule{
Enabled: true,
RuleID: "@tulir:maunium.net",
Actions: actions1,
}
actions2 := pushrules.PushActionArray{
{Action: pushrules.ActionNotify},
}
rule2 := &pushrules.PushRule{
Enabled: true,
RuleID: "@someone:maunium.net",
Actions: actions2,
}
actions3 := pushrules.PushActionArray{
{Action: pushrules.ActionCoalesce},
}
rule3 := &pushrules.PushRule{
Enabled: true,
RuleID: "@otheruser:matrix.org",
Actions: actions3,
}
ruleArray := pushrules.PushRuleArray{rule1, rule2, rule3}
ruleMap := ruleArray.SetTypeAndMap(pushrules.SenderRule)
assert.Equal(t, pushrules.SenderRule, ruleMap.Type)
for _, rule := range ruleArray {
assert.Equal(t, rule, ruleMap.Map[rule.RuleID])
}
newRuleArray := ruleMap.Unmap()
for _, rule := range ruleArray {
assert.Contains(t, newRuleArray, rule)
}
}

View File

@ -1,195 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules_test
import (
"github.com/stretchr/testify/assert"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/mautrix"
"testing"
)
func TestPushRule_Match_Conditions(t *testing.T) {
cond1 := newMatchPushCondition("content.msgtype", "m.emote")
cond2 := newMatchPushCondition("content.body", "*pushrules")
rule := &pushrules.PushRule{
Type: pushrules.OverrideRule,
Enabled: true,
Conditions: []*pushrules.PushCondition{cond1, cond2},
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
Raw: map[string]interface{}{
"msgtype": "m.emote",
"body": "is testing pushrules",
},
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.True(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Conditions_Disabled(t *testing.T) {
cond1 := newMatchPushCondition("content.msgtype", "m.emote")
cond2 := newMatchPushCondition("content.body", "*pushrules")
rule := &pushrules.PushRule{
Type: pushrules.OverrideRule,
Enabled: false,
Conditions: []*pushrules.PushCondition{cond1, cond2},
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
Raw: map[string]interface{}{
"msgtype": "m.emote",
"body": "is testing pushrules",
},
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.False(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Conditions_FailIfOneFails(t *testing.T) {
cond1 := newMatchPushCondition("content.msgtype", "m.emote")
cond2 := newMatchPushCondition("content.body", "*pushrules")
rule := &pushrules.PushRule{
Type: pushrules.OverrideRule,
Enabled: true,
Conditions: []*pushrules.PushCondition{cond1, cond2},
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
Raw: map[string]interface{}{
"msgtype": "m.text",
"body": "I'm testing pushrules",
},
MsgType: mautrix.MsgText,
Body: "I'm testing pushrules",
})
assert.False(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Content(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.ContentRule,
Enabled: true,
Pattern: "is testing*",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is testing pushrules",
})
assert.True(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Content_Fail(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.ContentRule,
Enabled: true,
Pattern: "is testing*",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is not testing pushrules",
})
assert.False(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Content_ImplicitGlob(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.ContentRule,
Enabled: true,
Pattern: "testing",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "is not testing pushrules",
})
assert.True(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Content_IllegalGlob(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.ContentRule,
Enabled: true,
Pattern: "this is not a valid glo[b",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{
MsgType: mautrix.MsgEmote,
Body: "this is not a valid glob",
})
assert.False(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Room(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!fakeroom:maunium.net",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{})
assert.True(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Room_Fail(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "!otherroom:maunium.net",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{})
assert.False(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Sender(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.SenderRule,
Enabled: true,
RuleID: "@tulir:maunium.net",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{})
assert.True(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_Sender_Fail(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.RoomRule,
Enabled: true,
RuleID: "@someone:matrix.org",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{})
assert.False(t, rule.Match(blankTestRoom, event))
}
func TestPushRule_Match_UnknownTypeAlwaysFail(t *testing.T) {
rule := &pushrules.PushRule{
Type: pushrules.PushRuleType("foobar"),
Enabled: true,
RuleID: "@someone:matrix.org",
}
event := newFakeEvent(mautrix.EventMessage, mautrix.Content{})
assert.False(t, rule.Match(blankTestRoom, event))
}

View File

@ -1,98 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package pushrules
import (
"encoding/json"
"maunium.net/go/mautrix"
)
type PushRuleset struct {
Override PushRuleArray
Content PushRuleArray
Room PushRuleMap
Sender PushRuleMap
Underride PushRuleArray
}
type rawPushRuleset struct {
Override PushRuleArray `json:"override"`
Content PushRuleArray `json:"content"`
Room PushRuleArray `json:"room"`
Sender PushRuleArray `json:"sender"`
Underride PushRuleArray `json:"underride"`
}
// UnmarshalJSON parses JSON into this PushRuleset.
//
// For override, sender and underride push rule arrays, the type is added
// to each PushRule and the array is used as-is.
//
// For room and sender push rule arrays, the type is added to each PushRule
// and the array is converted to a map with the rule ID as the key and the
// PushRule as the value.
func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) {
data := rawPushRuleset{}
err = json.Unmarshal(raw, &data)
if err != nil {
return
}
rs.Override = data.Override.SetType(OverrideRule)
rs.Content = data.Content.SetType(ContentRule)
rs.Room = data.Room.SetTypeAndMap(RoomRule)
rs.Sender = data.Sender.SetTypeAndMap(SenderRule)
rs.Underride = data.Underride.SetType(UnderrideRule)
return
}
// MarshalJSON is the reverse of UnmarshalJSON()
func (rs *PushRuleset) MarshalJSON() ([]byte, error) {
data := rawPushRuleset{
Override: rs.Override,
Content: rs.Content,
Room: rs.Room.Unmap(),
Sender: rs.Sender.Unmap(),
Underride: rs.Underride,
}
return json.Marshal(&data)
}
// DefaultPushActions is the value returned if none of the rule
// collections in a Ruleset match the event given to GetActions()
var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}}
// GetActions matches the given event against all of the push rule
// collections in this push ruleset in the order of priority as
// specified in spec section 11.12.1.4.
func (rs *PushRuleset) GetActions(room Room, event *mautrix.Event) (match PushActionArray) {
// Add push rule collections to array in priority order
arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride}
// Loop until one of the push rule collections matches the room/event combo.
for _, pra := range arrays {
if pra == nil {
continue
}
if match = pra.GetActions(room, event); match != nil {
// Match found, return it.
return
}
}
// No match found, return default actions.
return DefaultPushActions
}

View File

@ -27,6 +27,8 @@ import (
sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/debug"
)
@ -54,25 +56,27 @@ type RoomTag struct {
}
type UnreadMessage struct {
EventID string
EventID id.EventID
Counted bool
Highlight bool
}
type Member struct {
mautrix.Member
event.Member
// The user who sent the membership event
Sender string `json:"-"`
Sender id.UserID `json:"-"`
}
// Room represents a single Matrix room.
type Room struct {
// The room ID.
ID string
ID id.RoomID
// Whether or not the user has left the room.
HasLeft bool
// Whether or not the room is encrypted.
Encrypted bool
// The first batch of events that has been fetched for this room.
// Used for fetching additional history.
@ -80,14 +84,14 @@ type Room struct {
// The last_batch field from the most recent sync. Used for fetching member lists.
LastPrevBatch string
// The MXID of the user whose session this room was created for.
SessionUserID string
SessionUserID id.UserID
SessionMember *Member
// The number of unread messages that were notified about.
UnreadMessages []UnreadMessage
unreadCountCache *int
highlightCache *bool
lastMarkedRead string
lastMarkedRead id.EventID
// Whether or not this room is marked as a direct chat.
IsDirect bool
@ -101,10 +105,10 @@ type Room struct {
// Whether or not the members for this room have been fetched from the server.
MembersFetched bool
// Room state cache.
state map[mautrix.EventType]map[string]*mautrix.Event
state map[event.Type]map[string]*event.Event
// MXID -> Member cache calculated from membership events.
memberCache map[string]*Member
exMemberCache map[string]*Member
memberCache map[id.UserID]*Member
exMemberCache map[id.UserID]*Member
// The first two non-SessionUserID members in the room. Calculated at
// the same time as memberCache.
firstMemberCache *Member
@ -117,11 +121,11 @@ type Room struct {
// The topic of the room. Directly fetched from the m.room.topic state event.
topicCache string
// The canonical alias of the room. Directly fetched from the m.room.canonical_alias state event.
CanonicalAliasCache string
CanonicalAliasCache id.RoomAlias
// Whether or not the room has been tombstoned.
replacedCache bool
// The room ID that replaced this room.
replacedByCache *string
replacedByCache *id.RoomID
// Path for state store file.
path string
@ -174,7 +178,7 @@ func (room *Room) load() {
return
}
debug.Print("Loading state for room", room.ID, "from disk")
room.state = make(map[mautrix.EventType]map[string]*mautrix.Event)
room.state = make(map[event.Type]map[string]*event.Event)
file, err := os.OpenFile(room.path, os.O_RDONLY, 0600)
if err != nil {
if !os.IsNotExist(err) {
@ -265,7 +269,7 @@ func (room *Room) Save() {
}
// MarkRead clears the new message statuses on this room.
func (room *Room) MarkRead(eventID string) bool {
func (room *Room) MarkRead(eventID id.EventID) bool {
room.lock.Lock()
defer room.lock.Unlock()
if room.lastMarkedRead == eventID {
@ -319,7 +323,7 @@ func (room *Room) HasNewMessages() bool {
return len(room.UnreadMessages) > 0
}
func (room *Room) AddUnread(eventID string, counted, highlight bool) {
func (room *Room) AddUnread(eventID id.EventID, counted, highlight bool) {
room.lock.Lock()
defer room.lock.Unlock()
room.UnreadMessages = append(room.UnreadMessages, UnreadMessage{
@ -341,18 +345,25 @@ func (room *Room) AddUnread(eventID string, counted, highlight bool) {
}
}
var (
tagDirect = RoomTag{"net.maunium.gomuks.fake.direct", "0.5"}
tagInvite = RoomTag{"net.maunium.gomuks.fake.invite", "0.5"}
tagDefault = RoomTag{"", "0.5"}
tagLeave = RoomTag{"net.maunium.gomuks.fake.leave", "0.5"}
)
func (room *Room) Tags() []RoomTag {
room.lock.RLock()
defer room.lock.RUnlock()
if len(room.RawTags) == 0 {
if room.IsDirect {
return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}}
} else if room.SessionMember != nil && room.SessionMember.Membership == mautrix.MembershipInvite {
return []RoomTag{{"net.maunium.gomuks.fake.invite", "0.5"}}
} else if room.SessionMember != nil && room.SessionMember.Membership != mautrix.MembershipJoin {
return []RoomTag{{"net.maunium.gomuks.fake.leave", "0.5"}}
return []RoomTag{tagDirect}
} else if room.SessionMember != nil && room.SessionMember.Membership == event.MembershipInvite {
return []RoomTag{tagInvite}
} else if room.SessionMember != nil && room.SessionMember.Membership != event.MembershipJoin {
return []RoomTag{tagLeave}
}
return []RoomTag{{"", "0.5"}}
return []RoomTag{tagDefault}
}
return room.RawTags
}
@ -374,46 +385,46 @@ func (room *Room) UpdateSummary(summary mautrix.LazyLoadSummary) {
// UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination.
func (room *Room) UpdateState(event *mautrix.Event) {
if event.StateKey == nil {
func (room *Room) UpdateState(evt *event.Event) {
if evt.StateKey == nil {
panic("Tried to UpdateState() event with no state key.")
}
room.Load()
room.lock.Lock()
defer room.lock.Unlock()
room.changed = true
_, exists := room.state[event.Type]
_, exists := room.state[evt.Type]
if !exists {
room.state[event.Type] = make(map[string]*mautrix.Event)
room.state[evt.Type] = make(map[string]*event.Event)
}
switch event.Type {
case mautrix.StateRoomName:
room.NameCache = event.Content.Name
switch evt.Type {
case event.StateRoomName:
room.NameCache = evt.Content.Name
room.nameCacheSource = ExplicitRoomName
case mautrix.StateCanonicalAlias:
case event.StateCanonicalAlias:
if room.nameCacheSource <= CanonicalAliasRoomName {
room.NameCache = event.Content.Alias
room.NameCache = string(evt.Content.Alias)
room.nameCacheSource = CanonicalAliasRoomName
}
room.CanonicalAliasCache = event.Content.Alias
case mautrix.StateMember:
room.CanonicalAliasCache = evt.Content.Alias
case event.StateMember:
if room.nameCacheSource <= MemberRoomName {
room.NameCache = ""
}
room.updateMemberState(event)
case mautrix.StateTopic:
room.topicCache = event.Content.Topic
room.updateMemberState(evt)
case event.StateTopic:
room.topicCache = evt.Content.Topic
}
if event.Type != mautrix.StateMember {
debug.Printf("Updating state %s#%s for %s", event.Type.String(), event.GetStateKey(), room.ID)
if evt.Type != event.StateMember {
debug.Printf("Updating state %s#%s for %s", evt.Type.String(), evt.GetStateKey(), room.ID)
}
room.state[event.Type][*event.StateKey] = event
room.state[evt.Type][*evt.StateKey] = evt
}
func (room *Room) updateMemberState(event *mautrix.Event) {
userID := event.GetStateKey()
func (room *Room) updateMemberState(event *event.Event) {
userID := id.UserID(event.GetStateKey())
if userID == room.SessionUserID {
debug.Print("Updating session user state:", string(event.Content.VeryRaw))
room.SessionMember = room.eventToMember(userID, event.Sender, &event.Content)
@ -442,7 +453,7 @@ func (room *Room) updateMemberState(event *mautrix.Event) {
}
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event {
func (room *Room) GetStateEvent(eventType event.Type, stateKey string) *event.Event {
room.Load()
room.lock.RLock()
defer room.lock.RUnlock()
@ -452,7 +463,7 @@ func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *m
}
// getStateEvents returns the state events for the given type.
func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event {
func (room *Room) getStateEvents(eventType event.Type) map[string]*event.Event {
stateEventMap, _ := room.state[eventType]
return stateEventMap
}
@ -460,7 +471,7 @@ func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautri
// GetTopic returns the topic of the room.
func (room *Room) GetTopic() string {
if len(room.topicCache) == 0 {
topicEvt := room.GetStateEvent(mautrix.StateTopic, "")
topicEvt := room.GetStateEvent(event.StateTopic, "")
if topicEvt != nil {
room.topicCache = topicEvt.Content.Topic
}
@ -468,9 +479,9 @@ func (room *Room) GetTopic() string {
return room.topicCache
}
func (room *Room) GetCanonicalAlias() string {
func (room *Room) GetCanonicalAlias() id.RoomAlias {
if len(room.CanonicalAliasCache) == 0 {
canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "")
canonicalAliasEvt := room.GetStateEvent(event.StateCanonicalAlias, "")
if canonicalAliasEvt != nil {
room.CanonicalAliasCache = canonicalAliasEvt.Content.Alias
} else {
@ -485,7 +496,7 @@ func (room *Room) GetCanonicalAlias() string {
// updateNameFromNameEvent updates the room display name to be the name set in the name event.
func (room *Room) updateNameFromNameEvent() {
nameEvt := room.GetStateEvent(mautrix.StateRoomName, "")
nameEvt := room.GetStateEvent(event.StateRoomName, "")
if nameEvt != nil {
room.NameCache = nameEvt.Content.Name
}
@ -528,7 +539,7 @@ func (room *Room) updateNameCache() {
room.nameCacheSource = ExplicitRoomName
}
if len(room.NameCache) == 0 {
room.NameCache = room.GetCanonicalAlias()
room.NameCache = string(room.GetCanonicalAlias())
room.nameCacheSource = CanonicalAliasRoomName
}
if len(room.NameCache) == 0 {
@ -548,8 +559,8 @@ func (room *Room) GetTitle() string {
func (room *Room) IsReplaced() bool {
if room.replacedByCache == nil {
evt := room.GetStateEvent(mautrix.StateTombstone, "")
var replacement string
evt := room.GetStateEvent(event.StateTombstone, "")
var replacement id.RoomID
if evt != nil {
replacement = evt.Content.ReplacementRoom
}
@ -559,18 +570,18 @@ func (room *Room) IsReplaced() bool {
return room.replacedCache
}
func (room *Room) ReplacedBy() string {
func (room *Room) ReplacedBy() id.RoomID {
if room.replacedByCache == nil {
room.IsReplaced()
}
return *room.replacedByCache
}
func (room *Room) eventToMember(userID string, sender string, content *mautrix.Content) *Member {
func (room *Room) eventToMember(userID, sender id.UserID, content *event.Content) *Member {
member := content.Member
member.Membership = content.Membership
if len(member.Displayname) == 0 {
member.Displayname = userID
member.Displayname = string(userID)
}
return &Member{
Member: member,
@ -578,7 +589,7 @@ func (room *Room) eventToMember(userID string, sender string, content *mautrix.C
}
}
func (room *Room) updateNthMemberCache(userID string, member *Member) {
func (room *Room) updateNthMemberCache(userID id.UserID, member *Member) {
if userID != room.SessionUserID {
if room.firstMemberCache == nil {
room.firstMemberCache = member
@ -589,19 +600,20 @@ func (room *Room) updateNthMemberCache(userID string, member *Member) {
}
// createMemberCache caches all member events into a easily processable MXID -> *Member map.
func (room *Room) createMemberCache() map[string]*Member {
func (room *Room) createMemberCache() map[id.UserID]*Member {
if len(room.memberCache) > 0 {
return room.memberCache
}
cache := make(map[string]*Member)
exCache := make(map[string]*Member)
cache := make(map[id.UserID]*Member)
exCache := make(map[id.UserID]*Member)
room.lock.RLock()
events := room.getStateEvents(mautrix.StateMember)
memberEvents := room.getStateEvents(event.StateMember)
room.firstMemberCache = nil
room.secondMemberCache = nil
if events != nil {
for userID, event := range events {
member := room.eventToMember(userID, event.Sender, &event.Content)
if memberEvents != nil {
for userIDStr, evt := range memberEvents {
userID := id.UserID(userIDStr)
member := room.eventToMember(userID, evt.Sender, &evt.Content)
if member.Membership.IsInviteOrJoin() {
cache[userID] = member
room.updateNthMemberCache(userID, member)
@ -631,7 +643,7 @@ func (room *Room) createMemberCache() map[string]*Member {
//
// The members are returned from the cache.
// If the cache is empty, it is updated first.
func (room *Room) GetMembers() map[string]*Member {
func (room *Room) GetMembers() map[id.UserID]*Member {
room.Load()
room.createMemberCache()
return room.memberCache
@ -639,7 +651,7 @@ func (room *Room) GetMembers() map[string]*Member {
// GetMember returns the member with the given MXID.
// If the member doesn't exist, nil is returned.
func (room *Room) GetMember(userID string) *Member {
func (room *Room) GetMember(userID id.UserID) *Member {
if userID == room.SessionUserID && room.SessionMember != nil {
return room.SessionMember
}
@ -660,16 +672,27 @@ func (room *Room) GetMember(userID string) *Member {
return nil
}
func (room *Room) GetMemberCount() int {
if room.memberCache == nil && room.Summary.JoinedMemberCount != nil {
return *room.Summary.JoinedMemberCount
}
return len(room.GetMembers())
}
// GetSessionOwner returns the ID of the user whose session this room was created for.
func (room *Room) GetSessionOwner() string {
return room.SessionUserID
func (room *Room) GetOwnDisplayname() string {
member := room.GetMember(room.SessionUserID)
if member != nil {
return member.Displayname
}
return ""
}
// NewRoom creates a new Room with the given ID
func NewRoom(roomID string, cache *RoomCache) *Room {
func NewRoom(roomID id.RoomID, cache *RoomCache) *Room {
return &Room{
ID: roomID,
state: make(map[mautrix.EventType]map[string]*mautrix.Event),
state: make(map[event.Type]map[string]*event.Event),
path: cache.roomPath(roomID),
cache: cache,

View File

@ -1,237 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package rooms_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix"
)
func TestNewRoom_DefaultValues(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
assert.Equal(t, "!test:maunium.net", room.ID)
assert.Equal(t, "@tulir:maunium.net", room.SessionUserID)
assert.Empty(t, room.GetMembers())
assert.Equal(t, "Empty room", room.GetTitle())
assert.Empty(t, room.GetAliases())
assert.Empty(t, room.GetCanonicalAlias())
assert.Empty(t, room.GetTopic())
assert.Nil(t, room.GetMember(room.GetSessionOwner()))
}
func TestRoom_GetCanonicalAlias(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.UpdateState(&mautrix.Event{
Type: mautrix.StateCanonicalAlias,
Content: mautrix.Content{
Alias: "#foo:maunium.net",
},
})
assert.Equal(t, "#foo:maunium.net", room.GetCanonicalAlias())
}
func TestRoom_GetTopic(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.UpdateState(&mautrix.Event{
Type: mautrix.StateTopic,
Content: mautrix.Content{
Topic: "test topic",
},
})
assert.Equal(t, "test topic", room.GetTopic())
}
func TestRoom_Tags_Empty(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
assert.Empty(t, room.RawTags)
tags := room.Tags()
assert.Len(t, tags, 1)
assert.Equal(t, "", tags[0].Tag)
assert.Equal(t, "0.5", tags[0].Order)
}
func TestRoom_Tags_NotEmpty(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.RawTags = []rooms.RoomTag{{Tag: "foo", Order: "1"}, {Tag: "bar", Order: "1"}}
tags := room.Tags()
assert.Equal(t, room.RawTags, tags)
}
func TestRoom_GetAliases(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addAliases(room)
aliases := room.GetAliases()
assert.Contains(t, aliases, "#bar:maunium.net")
assert.Contains(t, aliases, "#test:maunium.net")
assert.Contains(t, aliases, "#foo:matrix.org")
assert.Contains(t, aliases, "#test:matrix.org")
}
func addName(room *rooms.Room) {
room.UpdateState(&mautrix.Event{
Type: mautrix.StateRoomName,
Content: mautrix.Content{
Name: "Test room",
},
})
}
func addCanonicalAlias(room *rooms.Room) {
room.UpdateState(&mautrix.Event{
Type: mautrix.StateCanonicalAlias,
Content: mautrix.Content{
Alias: "#foo:maunium.net",
},
})
}
func addAliases(room *rooms.Room) {
server1 := "maunium.net"
room.UpdateState(&mautrix.Event{
Type: mautrix.StateAliases,
StateKey: &server1,
Content: mautrix.Content{
Aliases: []string{"#bar:maunium.net", "#test:maunium.net", "#foo:maunium.net"},
},
})
server2 := "matrix.org"
room.UpdateState(&mautrix.Event{
Type: mautrix.StateAliases,
StateKey: &server2,
Content: mautrix.Content{
Aliases: []string{"#foo:matrix.org", "#test:matrix.org"},
},
})
}
func addMembers(room *rooms.Room, count int) {
user1 := "@tulir:maunium.net"
room.UpdateState(&mautrix.Event{
Type: mautrix.StateMember,
StateKey: &user1,
Content: mautrix.Content{
Member: mautrix.Member{
Displayname: "tulir",
Membership: mautrix.MembershipJoin,
},
},
})
for i := 1; i < count; i++ {
userN := fmt.Sprintf("@user_%d:matrix.org", i+1)
content := mautrix.Content{
Member: mautrix.Member{
Membership: mautrix.MembershipJoin,
},
}
if i%2 == 1 {
content.Displayname = fmt.Sprintf("User #%d", i+1)
}
if i%5 == 0 {
content.Membership = mautrix.MembershipInvite
}
room.UpdateState(&mautrix.Event{
Type: mautrix.StateMember,
StateKey: &userN,
Content: content,
})
}
}
func TestRoom_GetMembers(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 6)
members := room.GetMembers()
assert.Len(t, members, 6)
}
func TestRoom_GetMember(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 6)
assert.NotNil(t, room.GetMember("@user_2:matrix.org"))
assert.NotNil(t, room.GetMember("@tulir:maunium.net"))
assert.Equal(t, "@tulir:maunium.net", room.GetSessionOwner())
}
func TestRoom_GetTitle_ExplicitName(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 4)
addName(room)
addCanonicalAlias(room)
addAliases(room)
assert.Equal(t, "Test room", room.GetTitle())
}
func TestRoom_GetTitle_CanonicalAlias(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 4)
addCanonicalAlias(room)
addAliases(room)
assert.Equal(t, "#foo:maunium.net", room.GetTitle())
}
func TestRoom_GetTitle_FirstAlias(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 2)
addAliases(room)
assert.Equal(t, "#bar:maunium.net", room.GetTitle())
}
func TestRoom_GetTitle_Members_Empty(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 1)
assert.Equal(t, "Empty room", room.GetTitle())
}
func TestRoom_GetTitle_Members_OneToOne(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 2)
assert.Equal(t, "User #2", room.GetTitle())
}
func TestRoom_GetTitle_Members_GroupChat(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
addMembers(room, 76)
assert.Contains(t, room.GetTitle(), " and 74 others")
}
func TestRoom_MarkRead(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.AddUnread("foo", true, false)
assert.Equal(t, 1, room.UnreadCount())
assert.False(t, room.Highlighted())
room.AddUnread("bar", true, false)
assert.Equal(t, 2, room.UnreadCount())
assert.False(t, room.Highlighted())
room.AddUnread("asd", false, true)
assert.Equal(t, 2, room.UnreadCount())
assert.True(t, room.Highlighted())
room.MarkRead("asd")
assert.Empty(t, room.UnreadMessages)
}

View File

@ -27,6 +27,7 @@ import (
sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/gomuks/debug"
"maunium.net/go/mautrix/id"
)
// RoomCache contains room state info in a hashmap and linked list.
@ -37,15 +38,15 @@ type RoomCache struct {
directory string
maxSize int
maxAge int64
getOwner func() string
getOwner func() id.UserID
Map map[string]*Room
Map map[id.RoomID]*Room
head *Room
tail *Room
size int
}
func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() string) *RoomCache {
func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() id.UserID) *RoomCache {
return &RoomCache{
listPath: listPath,
directory: directory,
@ -53,7 +54,7 @@ func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwne
maxAge: maxAge,
getOwner: getOwner,
Map: make(map[string]*Room),
Map: make(map[id.RoomID]*Room),
}
}
@ -88,7 +89,7 @@ func (cache *RoomCache) LoadList() error {
}
// Read list
cache.Map = make(map[string]*Room, size)
cache.Map = make(map[id.RoomID]*Room, size)
for i := 0; i < size; i++ {
room := &Room{}
err = dec.Decode(room)
@ -147,7 +148,7 @@ func (cache *RoomCache) SaveList() error {
return nil
}
func (cache *RoomCache) Touch(roomID string) {
func (cache *RoomCache) Touch(roomID id.RoomID) {
cache.Lock()
node, ok := cache.Map[roomID]
if !ok || node == nil {
@ -174,14 +175,14 @@ func (cache *RoomCache) touch(node *Room) {
node.touch = time.Now().Unix()
}
func (cache *RoomCache) Get(roomID string) *Room {
func (cache *RoomCache) Get(roomID id.RoomID) *Room {
cache.Lock()
node := cache.get(roomID)
cache.Unlock()
return node
}
func (cache *RoomCache) GetOrCreate(roomID string) *Room {
func (cache *RoomCache) GetOrCreate(roomID id.RoomID) *Room {
cache.Lock()
node := cache.get(roomID)
if node == nil {
@ -192,7 +193,7 @@ func (cache *RoomCache) GetOrCreate(roomID string) *Room {
return node
}
func (cache *RoomCache) get(roomID string) *Room {
func (cache *RoomCache) get(roomID id.RoomID) *Room {
node, ok := cache.Map[roomID]
if ok && node != nil {
return node
@ -215,11 +216,11 @@ func (cache *RoomCache) Put(room *Room) {
node.Save()
}
func (cache *RoomCache) roomPath(roomID string) string {
return filepath.Join(cache.directory, roomID+".gob.gz")
func (cache *RoomCache) roomPath(roomID id.RoomID) string {
return filepath.Join(cache.directory, string(roomID)+".gob.gz")
}
func (cache *RoomCache) Load(roomID string) *Room {
func (cache *RoomCache) Load(roomID id.RoomID) *Room {
cache.Lock()
defer cache.Unlock()
node, ok := cache.Map[roomID]
@ -312,7 +313,7 @@ func (cache *RoomCache) Unload(node *Room) {
}
}
func (cache *RoomCache) newRoom(roomID string) *Room {
func (cache *RoomCache) newRoom(roomID id.RoomID) *Room {
node := NewRoom(roomID, cache)
cache.Map[node.ID] = node
return node

View File

@ -24,14 +24,16 @@ import (
"time"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/matrix/rooms"
)
type SyncerSession interface {
GetRoom(id string) *rooms.Room
GetUserID() string
GetRoom(id id.RoomID) *rooms.Room
GetUserID() id.UserID
}
type EventSource int
@ -45,6 +47,7 @@ const (
EventSourceTimeline
EventSourceState
EventSourceEphemeral
EventSourceToDevice
)
func (es EventSource) String() string {
@ -83,14 +86,14 @@ func (es EventSource) String() string {
return fmt.Sprintf("unknown (%d)", es)
}
type EventHandler func(source EventSource, event *mautrix.Event)
type EventHandler func(source EventSource, event *event.Event)
// GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively
// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
// pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information.
type GomuksSyncer struct {
Session SyncerSession
listeners map[mautrix.EventType][]EventHandler // event type to listeners array
listeners map[event.Type][]EventHandler // event type to listeners array
FirstSyncDone bool
InitDoneCallback func()
}
@ -99,7 +102,7 @@ type GomuksSyncer struct {
func NewGomuksSyncer(session SyncerSession) *GomuksSyncer {
return &GomuksSyncer{
Session: session,
listeners: make(map[mautrix.EventType][]EventHandler),
listeners: make(map[event.Type][]EventHandler),
FirstSyncDone: false,
}
}
@ -152,33 +155,44 @@ func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err
}
func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []json.RawMessage, source EventSource) {
for _, event := range events {
if source == EventSourcePresence {
debug.Print(string(event))
}
s.processSyncEvent(room, event, source)
for _, evt := range events {
s.processSyncEvent(room, evt, source)
}
}
func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, eventJSON json.RawMessage, source EventSource) {
event := &mautrix.Event{}
err := json.Unmarshal(eventJSON, event)
evt := &event.Event{}
err := json.Unmarshal(eventJSON, evt)
if err != nil {
debug.Print("Failed to unmarshal event: %v\n%s", err, string(eventJSON))
return
}
// Ensure the type class is correct. It's safe to mutate since it's not a pointer.
// Listeners are keyed by type structs, which means only the correct class will pass.
switch {
case evt.StateKey != nil:
evt.Type.Class = event.StateEventType
case source == EventSourcePresence, source & EventSourceEphemeral != 0:
evt.Type.Class = event.EphemeralEventType
case source & EventSourceAccountData != 0:
evt.Type.Class = event.AccountDataEventType
case source == EventSourceToDevice:
evt.Type.Class = event.ToDeviceEventType
default:
evt.Type.Class = event.MessageEventType
}
if room != nil {
event.RoomID = room.ID
if source&EventSourceState != 0 || (source&EventSourceTimeline != 0 && event.Type.IsState() && event.StateKey != nil) {
room.UpdateState(event)
evt.RoomID = room.ID
if evt.Type.IsState() {
room.UpdateState(evt)
}
}
s.notifyListeners(source, event)
s.notifyListeners(source, evt)
}
// OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks.
func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHandler) {
func (s *GomuksSyncer) OnEventType(eventType event.Type, callback EventHandler) {
_, exists := s.listeners[eventType]
if !exists {
s.listeners[eventType] = []EventHandler{}
@ -186,21 +200,13 @@ func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHa
s.listeners[eventType] = append(s.listeners[eventType], callback)
}
func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) {
if (event.Type.IsState() && source&EventSourceState == 0 && event.StateKey == nil) ||
(event.Type.IsAccountData() && source&EventSourceAccountData == 0) ||
(event.Type.IsEphemeral() && event.Type != mautrix.EphemeralEventPresence && source&EventSourceEphemeral == 0) ||
(event.Type == mautrix.EphemeralEventPresence && source&EventSourcePresence == 0) {
evtJson, _ := json.Marshal(event)
debug.Printf("Event of type %s received from mismatching source %s: %s", event.Type.String(), source.String(), string(evtJson))
return
}
listeners, exists := s.listeners[event.Type]
func (s *GomuksSyncer) notifyListeners(source EventSource, evt *event.Event) {
listeners, exists := s.listeners[evt.Type]
if !exists {
return
}
for _, fn := range listeners {
fn(source, event)
fn(source, evt)
}
}
@ -211,53 +217,51 @@ func (s *GomuksSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Dura
}
// GetFilterJSON returns a filter with a timeline limit of 50.
func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
func (s *GomuksSyncer) GetFilterJSON(_ id.UserID) json.RawMessage {
filter := &mautrix.Filter{
Room: mautrix.RoomFilter{
IncludeLeave: false,
State: mautrix.FilterPart{
LazyLoadMembers: true,
Types: []string{
"m.room.member",
"m.room.name",
"m.room.topic",
"m.room.canonical_alias",
"m.room.aliases",
"m.room.power_levels",
"m.room.tombstone",
Types: []event.Type{
event.StateMember,
event.StateRoomName,
event.StateTopic,
event.StateCanonicalAlias,
event.StatePowerLevels,
event.StateTombstone,
},
},
Timeline: mautrix.FilterPart{
LazyLoadMembers: true,
Types: []string{
"m.room.message",
"m.room.redaction",
"m.room.encrypted",
"m.sticker",
"m.reaction",
Types: []event.Type{
event.EventMessage,
event.EventRedaction,
event.EventEncrypted,
event.EventSticker,
event.EventReaction,
"m.room.member",
"m.room.name",
"m.room.topic",
"m.room.canonical_alias",
"m.room.aliases",
"m.room.power_levels",
"m.room.tombstone",
event.StateMember,
event.StateRoomName,
event.StateTopic,
event.StateCanonicalAlias,
event.StatePowerLevels,
event.StateTombstone,
},
// Limit: 50,
Limit: 50,
},
Ephemeral: mautrix.FilterPart{
Types: []string{"m.typing", "m.receipt"},
Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt},
},
AccountData: mautrix.FilterPart{
Types: []string{"m.tag"},
Types: []event.Type{event.AccountDataRoomTags},
},
},
AccountData: mautrix.FilterPart{
Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"},
Types: []event.Type{event.AccountDataPushRules, event.AccountDataDirectChats, AccountDataGomuksPreferences},
},
Presence: mautrix.FilterPart{
NotTypes: []string{"*"},
NotTypes: []event.Type{event.NewEventType("*")},
},
}
rawFilter, _ := json.Marshal(&filter)

View File

@ -1,219 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package matrix_test
import (
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/gomuks/matrix"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix"
)
func TestGomuksSyncer_ProcessResponse_Initial(t *testing.T) {
syncer := matrix.NewGomuksSyncer(&mockSyncerSession{})
var initDoneCalled = false
syncer.InitDoneCallback = func() {
initDoneCalled = true
}
syncer.ProcessResponse(newRespSync(), "")
assert.True(t, syncer.FirstSyncDone)
assert.True(t, initDoneCalled)
}
func TestGomuksSyncer_ProcessResponse(t *testing.T) {
mss := &mockSyncerSession{
userID: "@tulir:maunium.net",
rooms: map[string]*rooms.Room{
"!foo:maunium.net": {
Room: mautrix.NewRoom("!foo:maunium.net"),
},
"!bar:maunium.net": {
Room: mautrix.NewRoom("!bar:maunium.net"),
},
"!test:maunium.net": {
Room: mautrix.NewRoom("!test:maunium.net"),
},
},
}
ml := &mockListener{}
syncer := matrix.NewGomuksSyncer(mss)
syncer.OnEventType(mautrix.EventMessage, ml.receive)
syncer.OnEventType(mautrix.StateMember, ml.receive)
syncer.GetFilterJSON("@tulir:maunium.net")
joinEvt := &mautrix.Event{
ID: "!join:maunium.net",
Type: mautrix.StateMember,
Sender: "@tulir:maunium.net",
StateKey: ptr("̣@tulir:maunium.net"),
Content: mautrix.Content{
Membership: mautrix.MembershipJoin,
},
}
messageEvt := &mautrix.Event{
ID: "!msg:maunium.net",
Type: mautrix.EventMessage,
Content: mautrix.Content{
Body: "foo",
MsgType: mautrix.MsgText,
},
}
unhandledEvt := &mautrix.Event{
ID: "!unhandled:maunium.net",
Type: mautrix.EventType{Type: "m.room.unhandled_event"},
}
inviteEvt := &mautrix.Event{
ID: "!invite:matrix.org",
Type: mautrix.StateMember,
Sender: "@you:matrix.org",
StateKey: ptr("̣@tulir:maunium.net"),
Content: mautrix.Content{
Membership: mautrix.MembershipInvite,
},
}
leaveEvt := &mautrix.Event{
ID: "!leave:matrix.org",
Type: mautrix.StateMember,
Sender: "@you:matrix.org",
StateKey: ptr("̣@tulir:maunium.net"),
Content: mautrix.Content{
Membership: mautrix.MembershipLeave,
},
}
resp := newRespSync()
resp.Rooms.Join["!foo:maunium.net"] = join{
State: events{Events: []*mautrix.Event{joinEvt}},
Timeline: timeline{Events: []*mautrix.Event{messageEvt, unhandledEvt}},
}
resp.Rooms.Invite["!bar:maunium.net"] = struct {
State struct {
Events []*mautrix.Event `json:"events"`
} `json:"invite_state"`
}{
State: events{Events: []*mautrix.Event{inviteEvt}},
}
resp.Rooms.Leave["!test:maunium.net"] = struct {
State struct {
Events []*mautrix.Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
}{
State: events{Events: []*mautrix.Event{leaveEvt}},
}
syncer.ProcessResponse(resp, "since")
assert.Contains(t, ml.received, joinEvt, joinEvt.ID)
assert.Contains(t, ml.received, messageEvt, messageEvt.ID)
assert.NotContains(t, ml.received, unhandledEvt, unhandledEvt.ID)
assert.Contains(t, ml.received, inviteEvt, inviteEvt.ID)
assert.Contains(t, ml.received, leaveEvt, leaveEvt.ID)
}
type mockSyncerSession struct {
rooms map[string]*rooms.Room
userID string
}
func (mss *mockSyncerSession) GetRoom(id string) *rooms.Room {
return mss.rooms[id]
}
func (mss *mockSyncerSession) GetUserID() string {
return mss.userID
}
type events struct {
Events []*mautrix.Event `json:"events"`
}
type timeline struct {
Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
}
type join struct {
State struct {
Events []*mautrix.Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
Ephemeral struct {
Events []*mautrix.Event `json:"events"`
} `json:"ephemeral"`
AccountData struct {
Events []*mautrix.Event `json:"events"`
} `json:"account_data"`
}
func ptr(text string) *string {
return &text
}
type mockListener struct {
received []*mautrix.Event
}
func (ml *mockListener) receive(source matrix.EventSource, evt *mautrix.Event) {
ml.received = append(ml.received, evt)
}
func newRespSync() *mautrix.RespSync {
resp := &mautrix.RespSync{NextBatch: "123"}
resp.Rooms.Join = make(map[string]struct {
State struct {
Events []*mautrix.Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
Ephemeral struct {
Events []*mautrix.Event `json:"events"`
} `json:"ephemeral"`
AccountData struct {
Events []*mautrix.Event `json:"events"`
} `json:"account_data"`
})
resp.Rooms.Invite = make(map[string]struct {
State struct {
Events []*mautrix.Event `json:"events"`
} `json:"invite_state"`
})
resp.Rooms.Leave = make(map[string]struct {
State struct {
Events []*mautrix.Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
})
return resp
}