diff options
author | Tulir Asokan <tulir@maunium.net> | 2020-04-16 19:27:35 +0300 |
---|---|---|
committer | Tulir Asokan <tulir@maunium.net> | 2020-04-19 15:01:16 +0300 |
commit | 815190be147e575f12211c468f8121e5c60e6337 (patch) | |
tree | 8abd9b3d3952dbf767ca369ddad400db8a6f2d45 /matrix | |
parent | ff20c2c44f86b40f9214f1dc3d339584e48374f1 (diff) |
Update stuff and move pushrules to mautrix-go
Diffstat (limited to 'matrix')
-rw-r--r-- | matrix/history.go | 38 | ||||
-rw-r--r-- | matrix/matrix.go | 177 | ||||
-rw-r--r-- | matrix/matrix_test.go | 232 | ||||
-rw-r--r-- | matrix/muksevt/event.go (renamed from matrix/event/event.go) | 8 | ||||
-rw-r--r-- | matrix/pushrules/action.go | 134 | ||||
-rw-r--r-- | matrix/pushrules/action_test.go | 210 | ||||
-rw-r--r-- | matrix/pushrules/condition.go | 162 | ||||
-rw-r--r-- | matrix/pushrules/condition_displayname_test.go | 60 | ||||
-rw-r--r-- | matrix/pushrules/condition_eventmatch_test.go | 96 | ||||
-rw-r--r-- | matrix/pushrules/condition_membercount_test.go | 71 | ||||
-rw-r--r-- | matrix/pushrules/condition_test.go | 132 | ||||
-rw-r--r-- | matrix/pushrules/doc.go | 2 | ||||
-rw-r--r-- | matrix/pushrules/pushrules.go | 37 | ||||
-rw-r--r-- | matrix/pushrules/pushrules_test.go | 249 | ||||
-rw-r--r-- | matrix/pushrules/rule.go | 160 | ||||
-rw-r--r-- | matrix/pushrules/rule_array_test.go | 294 | ||||
-rw-r--r-- | matrix/pushrules/rule_test.go | 195 | ||||
-rw-r--r-- | matrix/pushrules/ruleset.go | 98 | ||||
-rw-r--r-- | matrix/rooms/room.go | 153 | ||||
-rw-r--r-- | matrix/rooms/room_test.go | 237 | ||||
-rw-r--r-- | matrix/rooms/roomcache.go | 27 | ||||
-rw-r--r-- | matrix/sync.go | 114 | ||||
-rw-r--r-- | matrix/sync_test.go | 219 |
23 files changed, 275 insertions, 2830 deletions
diff --git a/matrix/history.go b/matrix/history.go index bb480f0..8a80569 100644 --- a/matrix/history.go +++ b/matrix/history.go @@ -26,9 +26,10 @@ import ( sync "github.com/sasha-s/go-deadlock" bolt "go.etcd.io/bbolt" - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" ) type HistoryManager struct { @@ -87,7 +88,7 @@ func (hm *HistoryManager) Close() error { var ( EventNotFoundError = errors.New("event not found") - RoomNotFoundError = errors.New("room not found") + RoomNotFoundError = errors.New("room not found") ) func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []byte) (*bolt.Bucket, []byte, error) { @@ -103,7 +104,7 @@ func (hm *HistoryManager) getStreamIndex(tx *bolt.Tx, roomID []byte, eventID []b return stream, index, nil } -func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*event.Event, error) { +func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byte) (*muksevt.Event, error) { eventData := stream.Get(index) if eventData == nil || len(eventData) == 0 { return nil, EventNotFoundError @@ -111,7 +112,7 @@ func (hm *HistoryManager) getEvent(tx *bolt.Tx, stream *bolt.Bucket, index []byt return unmarshalEvent(eventData) } -func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Event, err error) { +func (hm *HistoryManager) Get(room *rooms.Room, eventID id.EventID) (evt *muksevt.Event, err error) { err = hm.db.View(func(tx *bolt.Tx) error { if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil { return err @@ -123,7 +124,7 @@ func (hm *HistoryManager) Get(room *rooms.Room, eventID string) (evt *event.Even return } -func (hm *HistoryManager) Update(room *rooms.Room, eventID string, update func(evt *event.Event) error) error { +func (hm *HistoryManager) Update(room *rooms.Room, eventID id.EventID, update func(evt *muksevt.Event) error) error { return hm.db.Update(func(tx *bolt.Tx) error { if stream, index, err := hm.getStreamIndex(tx, []byte(room.ID), []byte(eventID)); err != nil { return err @@ -140,18 +141,18 @@ func (hm *HistoryManager) Update(room *rooms.Room, eventID string, update func(e }) } -func (hm *HistoryManager) Append(room *rooms.Room, events []*mautrix.Event) ([]*event.Event, error) { +func (hm *HistoryManager) Append(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) { return hm.store(room, events, true) } -func (hm *HistoryManager) Prepend(room *rooms.Room, events []*mautrix.Event) ([]*event.Event, error) { +func (hm *HistoryManager) Prepend(room *rooms.Room, events []*event.Event) ([]*muksevt.Event, error) { return hm.store(room, events, false) } -func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, append bool) ([]*event.Event, error) { +func (hm *HistoryManager) store(room *rooms.Room, events []*event.Event, append bool) ([]*muksevt.Event, error) { hm.Lock() defer hm.Unlock() - newEvents := make([]*event.Event, len(events)) + newEvents := make([]*muksevt.Event, len(events)) err := hm.db.Update(func(tx *bolt.Tx) error { streamPointers := tx.Bucket(bucketStreamPointers) rid := []byte(room.ID) @@ -177,7 +178,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen return err } for i, evt := range events { - newEvents[i] = event.Wrap(evt) + newEvents[i] = muksevt.Wrap(evt) if err := put(stream, eventIDs, newEvents[i], ptrStart+uint64(i)); err != nil { return err } @@ -198,7 +199,7 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen } eventCount := uint64(len(events)) for i, evt := range events { - newEvents[i] = event.Wrap(evt) + newEvents[i] = muksevt.Wrap(evt) if err := put(stream, eventIDs, newEvents[i], -ptrStart-uint64(i)); err != nil { return err } @@ -215,12 +216,11 @@ func (hm *HistoryManager) store(room *rooms.Room, events []*mautrix.Event, appen return newEvents, err } -func (hm *HistoryManager) Load(room *rooms.Room, num int) (events []*event.Event, err error) { +func (hm *HistoryManager) Load(room *rooms.Room, num int) (events []*muksevt.Event, err error) { hm.Lock() defer hm.Unlock() err = hm.db.View(func(tx *bolt.Tx) error { - rid := []byte(room.ID) - stream := tx.Bucket(bucketRoomStreams).Bucket(rid) + stream := tx.Bucket(bucketRoomStreams).Bucket([]byte(room.ID)) if stream == nil { return nil } @@ -265,7 +265,7 @@ func btoi(b []byte) uint64 { return binary.BigEndian.Uint64(b) } -func marshalEvent(evt *event.Event) ([]byte, error) { +func marshalEvent(evt *muksevt.Event) ([]byte, error) { var buf bytes.Buffer enc := gzip.NewWriter(&buf) if err := gob.NewEncoder(enc).Encode(evt); err != nil { @@ -277,8 +277,8 @@ func marshalEvent(evt *event.Event) ([]byte, error) { return buf.Bytes(), nil } -func unmarshalEvent(data []byte) (*event.Event, error) { - evt := &event.Event{} +func unmarshalEvent(data []byte) (*muksevt.Event, error) { + evt := &muksevt.Event{} if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil { return nil, err } else if err := gob.NewDecoder(cmpReader).Decode(evt); err != nil { @@ -290,7 +290,7 @@ func unmarshalEvent(data []byte) (*event.Event, error) { return evt, nil } -func put(streams, eventIDs *bolt.Bucket, evt *event.Event, key uint64) error { +func put(streams, eventIDs *bolt.Bucket, evt *muksevt.Event, key uint64) error { data, err := marshalEvent(evt) if err != nil { return err diff --git a/matrix/matrix.go b/matrix/matrix.go index 651f6bb..cd40a5a 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -36,15 +36,17 @@ import ( "github.com/pkg/errors" "maunium.net/go/gomuks/lib/open" - "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/mautrix/pushrules" ) // Container is a wrapper for a mautrix Client and some other stuff. @@ -96,7 +98,8 @@ func (c *Container) InitClient() error { c.client = nil } - var mxid, accessToken string + var mxid id.UserID + var accessToken string if len(c.config.AccessToken) > 0 { accessToken = c.config.AccessToken mxid = c.config.UserID @@ -180,7 +183,7 @@ func respondHTML(w http.ResponseWriter, status int, message string) { } func (c *Container) SingleSignOn() error { - loginURL := c.client.BuildURLWithQuery([]string{"login", "sso", "redirect"}, map[string]string{ + loginURL := c.client.BuildURLWithQuery(mautrix.URLPath{"login", "sso", "redirect"}, map[string]string{ "redirectUrl": "http://localhost:29325", }) err := open.Open(loginURL) @@ -267,7 +270,7 @@ func (c *Container) Stop() { // UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object. func (c *Container) UpdatePushRules() { debug.Print("Updating push rules...") - resp, err := pushrules.GetPushRules(c.client) + resp, err := c.client.GetPushRules() if err != nil { debug.Print("Failed to fetch push rules:", err) c.config.PushRules = &pushrules.PushRuleset{} @@ -285,7 +288,10 @@ func (c *Container) PushRules() *pushrules.PushRuleset { return c.config.PushRules } -var AccountDataGomuksPreferences = mautrix.NewEventType("net.maunium.gomuks.preferences") +var AccountDataGomuksPreferences = event.Type{ + Type: "net.maunium.gomuks.preferences", + Class: event.AccountDataEventType, +} // OnLogin initializes the syncer and updates the room list. func (c *Container) OnLogin() { @@ -295,21 +301,21 @@ func (c *Container) OnLogin() { debug.Print("Initializing syncer") c.syncer = NewGomuksSyncer(c.config) - c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventReaction, c.HandleMessage) - c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction) - c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateRoomName, c.HandleMessage) - c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership) - c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt) - c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping) - c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo) - c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules) - c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag) + c.syncer.OnEventType(event.EventMessage, c.HandleMessage) + c.syncer.OnEventType(event.EventEncrypted, c.HandleMessage) + c.syncer.OnEventType(event.EventSticker, c.HandleMessage) + c.syncer.OnEventType(event.EventReaction, c.HandleMessage) + c.syncer.OnEventType(event.EventRedaction, c.HandleRedaction) + c.syncer.OnEventType(event.StateAliases, c.HandleMessage) + c.syncer.OnEventType(event.StateCanonicalAlias, c.HandleMessage) + c.syncer.OnEventType(event.StateTopic, c.HandleMessage) + c.syncer.OnEventType(event.StateRoomName, c.HandleMessage) + c.syncer.OnEventType(event.StateMember, c.HandleMembership) + c.syncer.OnEventType(event.EphemeralEventReceipt, c.HandleReadReceipt) + c.syncer.OnEventType(event.EphemeralEventTyping, c.HandleTyping) + c.syncer.OnEventType(event.AccountDataDirectChats, c.HandleDirectChatInfo) + c.syncer.OnEventType(event.AccountDataPushRules, c.HandlePushRules) + c.syncer.OnEventType(event.AccountDataRoomTags, c.HandleTag) c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences) c.syncer.InitDoneCallback = func() { debug.Print("Initial sync done") @@ -372,7 +378,7 @@ func (c *Container) Start() { } } -func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) { +func (c *Container) HandlePreferences(source EventSource, evt *event.Event) { if source&EventSourceAccountData == 0 { return } @@ -395,18 +401,17 @@ func (c *Container) Preferences() *config.UserPreferences { func (c *Container) SendPreferencesToMatrix() { defer debug.Recover() debug.Print("Sending updated preferences:", c.config.Preferences) - u := c.client.BuildURL("user", c.config.UserID, "account_data", AccountDataGomuksPreferences.Type) + u := c.client.BuildURL("user", string(c.config.UserID), "account_data", AccountDataGomuksPreferences.Type) _, err := c.client.MakeRequest("PUT", u, &c.config.Preferences, nil) if err != nil { debug.Print("Failed to update preferences:", err) } } -func (c *Container) HandleRedaction(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleRedaction(source EventSource, evt *event.Event) { room := c.GetOrCreateRoom(evt.RoomID) - var redactedEvt *event.Event - err := c.history.Update(room, evt.Redacts, func(redacted *event.Event) error { - redacted.Unsigned.RedactedBy = evt.ID + var redactedEvt *muksevt.Event + err := c.history.Update(room, evt.Redacts, func(redacted *muksevt.Event) error { redacted.Unsigned.RedactedBecause = evt redactedEvt = redacted return nil @@ -430,9 +435,9 @@ func (c *Container) HandleRedaction(source EventSource, evt *mautrix.Event) { } } -func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *event.Event) { - var origEvt *event.Event - err := c.history.Update(room, editsID, func(evt *event.Event) error { +func (c *Container) HandleEdit(room *rooms.Room, editsID id.EventID, editEvent *muksevt.Event) { + var origEvt *muksevt.Event + err := c.history.Update(room, editsID, func(evt *muksevt.Event) error { evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent) origEvt = evt return nil @@ -456,10 +461,10 @@ func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *even } } -func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent *event.Event) { +func (c *Container) HandleReaction(room *rooms.Room, reactsTo id.EventID, reactEvent *muksevt.Event) { rel := reactEvent.Content.GetRelatesTo() - var origEvt *event.Event - err := c.history.Update(room, reactsTo, func(evt *event.Event) error { + var origEvt *muksevt.Event + err := c.history.Update(room, reactsTo, func(evt *muksevt.Event) error { if evt.Unsigned.Relations.Annotations.Map == nil { evt.Unsigned.Relations.Annotations.Map = make(map[string]int) } @@ -488,7 +493,7 @@ func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent } // HandleMessage is the event handler for the m.room.message timeline event. -func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { +func (c *Container) HandleMessage(source EventSource, mxEvent *event.Event) { room := c.GetOrCreateRoom(mxEvent.RoomID) if source&EventSourceLeave != 0 { room.HasLeft = true @@ -498,14 +503,14 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { } if editID := mxEvent.Content.GetRelatesTo().GetReplaceID(); len(editID) > 0 { - c.HandleEdit(room, editID, event.Wrap(mxEvent)) + c.HandleEdit(room, editID, muksevt.Wrap(mxEvent)) return - } else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == mautrix.EventReaction && len(reactionID) > 0 { - c.HandleReaction(room, reactionID, event.Wrap(mxEvent)) + } else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == event.EventReaction && len(reactionID) > 0 { + c.HandleReaction(room, reactionID, muksevt.Wrap(mxEvent)) return } - events, err := c.history.Append(room, []*mautrix.Event{mxEvent}) + events, err := c.history.Append(room, []*event.Event{mxEvent}) if err != nil { debug.Printf("Failed to add event %s to history: %v", mxEvent.ID, err) } @@ -549,7 +554,7 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { } // HandleMembership is the event handler for the m.room.member state event. -func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleMembership(source EventSource, evt *event.Event) { isLeave := source&EventSourceLeave != 0 isTimeline := source&EventSourceTimeline != 0 if isLeave { @@ -558,7 +563,7 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { isNonTimelineLeave := isLeave && !isTimeline if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { return - } else if evt.StateKey != nil && *evt.StateKey == c.config.UserID { + } else if evt.StateKey != nil && id.UserID(*evt.StateKey) == c.config.UserID { c.processOwnMembershipChange(evt) } else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) { // We don't care about other users' membership events in the initial sync or chats we've left. @@ -568,9 +573,9 @@ func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { c.HandleMessage(source, evt) } -func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { +func (c *Container) processOwnMembershipChange(evt *event.Event) { membership := evt.Content.Membership - prevMembership := mautrix.MembershipLeave + prevMembership := event.MembershipLeave if evt.Unsigned.PrevContent != nil { prevMembership = evt.Unsigned.PrevContent.Membership } @@ -603,7 +608,7 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { c.ui.Render() } -func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent string) { +func (c *Container) parseReadReceipt(evt *event.Event) (largestTimestampEvent id.EventID) { var largestTimestamp int64 for eventID, rawContent := range evt.Content.Raw { content, ok := rawContent.(map[string]interface{}) @@ -616,7 +621,7 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent continue } - myInfo, ok := mRead[c.config.UserID].(map[string]interface{}) + myInfo, ok := mRead[string(c.config.UserID)].(map[string]interface{}) if !ok { continue } @@ -624,13 +629,13 @@ func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent ts, ok := myInfo["ts"].(float64) if int64(ts) > largestTimestamp { largestTimestamp = int64(ts) - largestTimestampEvent = eventID + largestTimestampEvent = id.EventID(eventID) } } return } -func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleReadReceipt(source EventSource, evt *event.Event) { if source&EventSourceLeave != 0 { return } @@ -649,7 +654,7 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { } } -func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool { +func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]bool { directChats := make(map[*rooms.Room]bool) for _, rawRoomIDList := range evt.Content.Raw { roomIDList, ok := rawRoomIDList.([]interface{}) @@ -663,7 +668,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool continue } - room := c.GetOrCreateRoom(roomID) + room := c.GetOrCreateRoom(id.RoomID(roomID)) if room != nil && !room.HasLeft { directChats[room] = true } @@ -672,7 +677,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool return directChats } -func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandleDirectChatInfo(_ EventSource, evt *event.Event) { directChats := c.parseDirectChatInfo(evt) for _, room := range c.config.Rooms.Map { shouldBeDirect := directChats[room] @@ -686,7 +691,7 @@ func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) { } // HandlePushRules is the event handler for the m.push_rules account data event. -func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandlePushRules(_ EventSource, evt *event.Event) { debug.Print("Received updated push rules") var err error c.config.PushRules, err = pushrules.EventToPushRules(evt) @@ -698,7 +703,7 @@ func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) { } // HandleTag is the event handler for the m.tag account data event. -func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandleTag(_ EventSource, evt *event.Event) { debug.Printf("Received tags for %s: %s -- %s", evt.RoomID, evt.Content.RoomTags, string(evt.Content.VeryRaw)) room := c.GetOrCreateRoom(evt.RoomID) @@ -724,24 +729,24 @@ func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) { } // HandleTyping is the event handler for the m.typing event. -func (c *Container) HandleTyping(_ EventSource, evt *mautrix.Event) { +func (c *Container) HandleTyping(_ EventSource, evt *event.Event) { if !c.config.AuthCache.InitialSyncDone { return } c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs) } -func (c *Container) MarkRead(roomID, eventID string) { +func (c *Container) MarkRead(roomID id.RoomID, eventID id.EventID) { urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID) _, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil) } -func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, rel *ifc.Relation) *event.Event { - var content mautrix.Content +func (c *Container) PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, rel *ifc.Relation) *muksevt.Event { + var content event.Content if html != "" { - content = mautrix.Content{ + content = event.Content{ FormattedBody: html, - Format: mautrix.FormatHTML, + Format: event.FormatHTML, Body: text, MsgType: msgtype, } @@ -750,49 +755,49 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag content.MsgType = msgtype } - if rel != nil && rel.Type == mautrix.RelReplace { + if rel != nil && rel.Type == event.RelReplace { contentCopy := content content.NewContent = &contentCopy content.Body = "* " + content.Body if len(content.FormattedBody) > 0 { content.FormattedBody = "* " + content.FormattedBody } - content.RelatesTo = &mautrix.RelatesTo{ - Type: mautrix.RelReplace, + content.RelatesTo = &event.RelatesTo{ + Type: event.RelReplace, EventID: rel.Event.ID, } - } else if rel != nil && rel.Type == mautrix.RelReference { + } else if rel != nil && rel.Type == event.RelReference { content.SetReply(rel.Event.Event) } txnID := c.client.TxnID() - localEcho := event.Wrap(&mautrix.Event{ - ID: txnID, + localEcho := muksevt.Wrap(&event.Event{ + ID: id.EventID(txnID), Sender: c.config.UserID, - Type: mautrix.EventMessage, + Type: event.EventMessage, Timestamp: time.Now().UnixNano() / 1e6, RoomID: roomID, Content: content, - Unsigned: mautrix.Unsigned{ + Unsigned: event.Unsigned{ TransactionID: txnID, }, }) - localEcho.Gomuks.OutgoingState = event.StateLocalEcho - if rel != nil && rel.Type == mautrix.RelReplace { + localEcho.Gomuks.OutgoingState = muksevt.StateLocalEcho + if rel != nil && rel.Type == event.RelReplace { localEcho.ID = rel.Event.ID - localEcho.Gomuks.Edits = []*event.Event{localEcho} + localEcho.Gomuks.Edits = []*muksevt.Event{localEcho} } return localEcho } -func (c *Container) Redact(roomID, eventID, reason string) error { +func (c *Container) Redact(roomID id.RoomID, eventID id.EventID, reason string) error { defer debug.Recover() _, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason}) return err } // SendMessage sends the given event. -func (c *Container) SendEvent(event *event.Event) (string, error) { +func (c *Container) SendEvent(event *muksevt.Event) (id.EventID, error) { defer debug.Recover() c.client.UserTyping(event.RoomID, false, 0) @@ -804,13 +809,13 @@ func (c *Container) SendEvent(event *event.Event) (string, error) { return resp.EventID, nil } -func (c *Container) sendTypingAsync(roomID string, typing bool, timeout int64) { +func (c *Container) sendTypingAsync(roomID id.RoomID, typing bool, timeout int64) { defer debug.Recover() _, _ = c.client.UserTyping(roomID, typing, timeout) } // SendTyping sets whether or not the user is typing in the given room. -func (c *Container) SendTyping(roomID string, typing bool) { +func (c *Container) SendTyping(roomID id.RoomID, typing bool) { ts := time.Now().Unix() if (c.typing > ts && typing) || (c.typing == 0 && !typing) { return @@ -836,8 +841,8 @@ func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) } // JoinRoom makes the current user try to join the given room. -func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) { - resp, err := c.client.JoinRoom(roomID, server, nil) +func (c *Container) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) { + resp, err := c.client.JoinRoom(string(roomID), server, nil) if err != nil { return nil, err } @@ -848,7 +853,7 @@ func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) { } // LeaveRoom makes the current user leave the given room. -func (c *Container) LeaveRoom(roomID string) error { +func (c *Container) LeaveRoom(roomID id.RoomID) error { _, err := c.client.LeaveRoom(roomID) if err != nil { return err @@ -873,7 +878,7 @@ func (c *Container) FetchMembers(room *rooms.Room) error { } // GetHistory fetches room history. -func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, error) { +func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*muksevt.Event, error) { events, err := c.history.Load(room, limit) if err != nil { return nil, err @@ -893,7 +898,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err room.PrevBatch = resp.End c.config.Rooms.Put(room) if len(resp.Chunk) == 0 { - return []*event.Event{}, nil + return []*muksevt.Event{}, nil } events, err = c.history.Prepend(room, resp.Chunk) if err != nil { @@ -902,7 +907,7 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*event.Event, err return events, nil } -func (c *Container) GetEvent(room *rooms.Room, eventID string) (*event.Event, error) { +func (c *Container) GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error) { evt, err := c.history.Get(room, eventID) if err != nil && err != EventNotFoundError { debug.Printf("Failed to get event %s from local cache: %v", eventID, err) @@ -914,18 +919,18 @@ func (c *Container) GetEvent(room *rooms.Room, eventID string) (*event.Event, er if err != nil { return nil, err } - evt = event.Wrap(mxEvent) + evt = muksevt.Wrap(mxEvent) debug.Printf("Loaded event %s from server", eventID) return evt, nil } // GetOrCreateRoom gets the room instance stored in the session. -func (c *Container) GetOrCreateRoom(roomID string) *rooms.Room { +func (c *Container) GetOrCreateRoom(roomID id.RoomID) *rooms.Room { return c.config.Rooms.GetOrCreate(roomID) } // GetRoom gets the room instance stored in the session. -func (c *Container) GetRoom(roomID string) *rooms.Room { +func (c *Container) GetRoom(roomID id.RoomID) *rooms.Room { return c.config.Rooms.Get(roomID) } @@ -949,7 +954,7 @@ func cp(src, dst string) error { return out.Close() } -func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullPath string, err error) { +func (c *Container) DownloadToDisk(uri id.ContentURI, target string) (fullPath string, err error) { cachePath := c.GetCachePath(uri) if target == "" { fullPath = cachePath @@ -994,7 +999,7 @@ func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullP // Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors. // // The file will be either read from the media cache (if found) or downloaded from the server. -func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) { +func (c *Container) Download(uri id.ContentURI) (data []byte, err error) { cacheFile := c.GetCachePath(uri) var info os.FileInfo if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() { @@ -1008,7 +1013,7 @@ func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) { return } -func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string { +func (c *Container) GetDownloadURL(uri id.ContentURI) string { dlURL, _ := url.Parse(c.client.HomeserverURL.String()) if dlURL.Scheme == "" { dlURL.Scheme = "https" @@ -1017,7 +1022,7 @@ func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string { return dlURL.String() } -func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []byte, err error) { +func (c *Container) download(uri id.ContentURI, cacheFile string) (data []byte, err error) { var resp *http.Response resp, err = c.client.Client.Get(c.GetDownloadURL(uri)) if err != nil { @@ -1039,7 +1044,7 @@ func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []b // GetCachePath gets the path to the cached version of the given homeserver:fileID combination. // The file may or may not exist, use Download() to ensure it has been cached. -func (c *Container) GetCachePath(uri mautrix.ContentURI) string { +func (c *Container) GetCachePath(uri id.ContentURI) string { dir := filepath.Join(c.config.MediaDir, uri.Homeserver) err := os.MkdirAll(dir, 0700) diff --git a/matrix/matrix_test.go b/matrix/matrix_test.go deleted file mode 100644 index 05362b9..0000000 --- a/matrix/matrix_test.go +++ /dev/null @@ -1,232 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package matrix - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - - "maunium.net/go/gomuks/config" - "maunium.net/go/mautrix" -) - -func TestContainer_InitClient_Empty(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-0") - os.MkdirAll("/tmp/gomuks-mxtest-0", 0700) - cfg := config.NewConfig("/tmp/gomuks-mxtest-0", "/tmp/gomuks-mxtest-0") - cfg.HS = "https://matrix.org" - c := Container{config: cfg} - assert.Nil(t, c.InitClient()) -} - -func TestContainer_GetCachePath(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-1") - cfg := config.NewConfig("/tmp/gomuks-mxtest-1", "/tmp/gomuks-mxtest-1") - c := Container{config: cfg} - assert.Equal(t, "/tmp/gomuks-mxtest-1/media/maunium.net/foobar", c.GetCachePath("maunium.net", "foobar")) -} - -/* FIXME probably not applicable anymore -func TestContainer_SendMarkdownMessage_NoMarkdown(t *testing.T) { - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPut || !strings.HasPrefix(req.URL.Path, "/_matrix/client/r0/rooms/!foo:example.com/send/m.room.message/") { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - - body := parseBody(req) - assert.Equal(t, "m.text", body["msgtype"]) - assert.Equal(t, "test message", body["body"]) - return mockResponse(http.StatusOK, `{"event_id": "!foobar1:example.com"}`), nil - })} - - event := c.PrepareMarkdownMessage("!foo:example.com", "m.text", "test message") - evtID, err := c.SendEvent(event) - assert.Nil(t, err) - assert.Equal(t, "!foobar1:example.com", evtID) -}*/ - -func TestContainer_SendMarkdownMessage_WithMarkdown(t *testing.T) { - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPut || !strings.HasPrefix(req.URL.Path, "/_matrix/client/r0/rooms/!foo:example.com/send/m.room.message/") { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - - body := parseBody(req) - assert.Equal(t, "m.text", body["msgtype"]) - assert.Equal(t, "**formatted** test _message_", body["body"]) - assert.Equal(t, "<p><strong>formatted</strong> <u>test</u> <em>message</em></p>\n", body["formatted_body"]) - return mockResponse(http.StatusOK, `{"event_id": "!foobar2:example.com"}`), nil - }), config: &config.Config{UserID: "@user:example.com"}} - - event := c.PrepareMarkdownMessage("!foo:example.com", "m.text", "**formatted** <u>test</u> _message_") - evtID, err := c.SendEvent(event) - assert.Nil(t, err) - assert.Equal(t, "!foobar2:example.com", evtID) -} - -func TestContainer_SendTyping(t *testing.T) { - var calls []mautrix.ReqTyping - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPut || req.URL.Path != "/_matrix/client/r0/rooms/!foo:example.com/typing/@user:example.com" { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - - rawBody, err := ioutil.ReadAll(req.Body) - if err != nil { - return nil, err - } - - call := mautrix.ReqTyping{} - err = json.Unmarshal(rawBody, &call) - if err != nil { - return nil, err - } - calls = append(calls, call) - - return mockResponse(http.StatusOK, `{}`), nil - })} - - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", false) - c.SendTyping("!foo:example.com", true) - c.SendTyping("!foo:example.com", false) - assert.Len(t, calls, 4) - assert.True(t, calls[0].Typing) - assert.False(t, calls[1].Typing) - assert.True(t, calls[2].Typing) - assert.False(t, calls[3].Typing) -} - -func TestContainer_JoinRoom(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-2") - cfg := config.NewConfig("/tmp/gomuks-mxtest-2", "/tmp/gomuks-mxtest-2") - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method == http.MethodPost && req.URL.Path == "/_matrix/client/r0/join/!foo:example.com" { - return mockResponse(http.StatusOK, `{"room_id": "!foo:example.com"}`), nil - } else if req.Method == http.MethodPost && req.URL.Path == "/_matrix/client/r0/rooms/!foo:example.com/leave" { - return mockResponse(http.StatusOK, `{}`), nil - } - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - }), config: cfg} - - room, err := c.JoinRoom("!foo:example.com", "") - assert.Nil(t, err) - assert.Equal(t, "!foo:example.com", room.ID) - assert.False(t, room.HasLeft) - - err = c.LeaveRoom("!foo:example.com") - assert.Nil(t, err) - assert.True(t, room.HasLeft) -} - -func TestContainer_Download(t *testing.T) { - defer os.RemoveAll("/tmp/gomuks-mxtest-3") - cfg := config.NewConfig("/tmp/gomuks-mxtest-3", "/tmp/gomuks-mxtest-3") - cfg.LoadAll() - callCounter := 0 - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodGet || req.URL.Path != "/_matrix/media/v1/download/example.com/foobar" { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - callCounter++ - return mockResponse(http.StatusOK, `example file`), nil - }), config: cfg} - - // Check that download works - data, hs, id, err := c.Download("mxc://example.com/foobar") - assert.Equal(t, "example.com", hs) - assert.Equal(t, "foobar", id) - assert.Equal(t, 1, callCounter) - assert.Equal(t, []byte("example file"), data) - assert.Nil(t, err) - - // Check that cache works - data, _, _, err = c.Download("mxc://example.com/foobar") - assert.Nil(t, err) - assert.Equal(t, []byte("example file"), data) - assert.Equal(t, 1, callCounter) -} - -func TestContainer_Download_InvalidURL(t *testing.T) { - c := Container{} - data, hs, id, err := c.Download("mxc://invalid mxc") - assert.NotNil(t, err) - assert.Empty(t, id) - assert.Empty(t, hs) - assert.Empty(t, data) -} - -/* FIXME -func TestContainer_GetHistory(t *testing.T) { - c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodGet || req.URL.Path != "/_matrix/client/r0/rooms/!foo:maunium.net/messages" { - return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) - } - return mockResponse(http.StatusOK, `{"start": "123", "end": "456", "chunk": [{"event_id": "it works"}]}`), nil - })} - - history, prevBatch, err := c.GetHistory("!foo:maunium.net", "123", 5) - assert.Nil(t, err) - assert.Equal(t, "it works", history[0].ID) - assert.Equal(t, "456", prevBatch) -}*/ - -func mockClient(fn func(*http.Request) (*http.Response, error)) *mautrix.Client { - client, _ := mautrix.NewClient("https://example.com", "@user:example.com", "foobar") - client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}} - return client -} - -func parseBody(req *http.Request) map[string]interface{} { - rawBody, err := ioutil.ReadAll(req.Body) - if err != nil { - panic(err) - } - - data := make(map[string]interface{}) - - err = json.Unmarshal(rawBody, &data) - if err != nil { - panic(err) - } - - return data -} - -func mockResponse(status int, body string) *http.Response { - return &http.Response{ - StatusCode: status, - Body: ioutil.NopCloser(strings.NewReader(body)), - } -} - -type MockRoundTripper struct { - RT func(*http.Request) (*http.Response, error) -} - -func (t MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return t.RT(req) -} diff --git a/matrix/event/event.go b/matrix/muksevt/event.go index 8506c9c..9f7a3ce 100644 --- a/matrix/event/event.go +++ b/matrix/muksevt/event.go @@ -14,14 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -package event +package muksevt import ( - "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" ) type Event struct { - *mautrix.Event + *event.Event Gomuks GomuksContent `json:"-"` } @@ -33,7 +33,7 @@ func (evt *Event) SomewhatDangerousCopy() *Event { } } -func Wrap(event *mautrix.Event) *Event { +func Wrap(event *event.Event) *Event { return &Event{Event: event} } diff --git a/matrix/pushrules/action.go b/matrix/pushrules/action.go deleted file mode 100644 index 4637950..0000000 --- a/matrix/pushrules/action.go +++ /dev/null @@ -1,134 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import "encoding/json" - -// PushActionType is the type of a PushAction -type PushActionType string - -// The allowed push action types as specified in spec section 11.12.1.4.1. -const ( - ActionNotify PushActionType = "notify" - ActionDontNotify PushActionType = "dont_notify" - ActionCoalesce PushActionType = "coalesce" - ActionSetTweak PushActionType = "set_tweak" -) - -// PushActionTweak is the type of the tweak in SetTweak push actions. -type PushActionTweak string - -// The allowed tweak types as specified in spec section 11.12.1.4.1.1. -const ( - TweakSound PushActionTweak = "sound" - TweakHighlight PushActionTweak = "highlight" -) - -// PushActionArray is an array of PushActions. -type PushActionArray []*PushAction - -// PushActionArrayShould contains the important information parsed from a PushActionArray. -type PushActionArrayShould struct { - // Whether or not the array contained a Notify, DontNotify or Coalesce action type. - NotifySpecified bool - // Whether or not the event in question should trigger a notification. - Notify bool - // Whether or not the event in question should be highlighted. - Highlight bool - - // Whether or not the event in question should trigger a sound alert. - PlaySound bool - // The name of the sound to play if PlaySound is true. - SoundName string -} - -// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct. -func (actions PushActionArray) Should() (should PushActionArrayShould) { - for _, action := range actions { - switch action.Action { - case ActionNotify, ActionCoalesce: - should.Notify = true - should.NotifySpecified = true - case ActionDontNotify: - should.Notify = false - should.NotifySpecified = true - case ActionSetTweak: - switch action.Tweak { - case TweakHighlight: - var ok bool - should.Highlight, ok = action.Value.(bool) - if !ok { - // Highlight value not specified, so assume true since the tweak is set. - should.Highlight = true - } - case TweakSound: - should.SoundName = action.Value.(string) - should.PlaySound = len(should.SoundName) > 0 - } - } - } - return -} - -// PushAction is a single action that should be triggered when receiving a message. -type PushAction struct { - Action PushActionType - Tweak PushActionTweak - Value interface{} -} - -// UnmarshalJSON parses JSON into this PushAction. -// -// * If the JSON is a single string, the value is stored in the Action field. -// * If the JSON is an object with the set_tweak field, Action will be set to -// "set_tweak", Tweak will be set to the value of the set_tweak field and -// and Value will be set to the value of the value field. -// * In any other case, the function does nothing. -func (action *PushAction) UnmarshalJSON(raw []byte) error { - var data interface{} - - err := json.Unmarshal(raw, &data) - if err != nil { - return err - } - - switch val := data.(type) { - case string: - action.Action = PushActionType(val) - case map[string]interface{}: - tweak, ok := val["set_tweak"].(string) - if ok { - action.Action = ActionSetTweak - action.Tweak = PushActionTweak(tweak) - action.Value, _ = val["value"] - } - } - return nil -} - -// MarshalJSON is the reverse of UnmarshalJSON() -func (action *PushAction) MarshalJSON() (raw []byte, err error) { - if action.Action == ActionSetTweak { - data := map[string]interface{}{ - "set_tweak": action.Tweak, - "value": action.Value, - } - return json.Marshal(&data) - } - data := string(action.Action) - return json.Marshal(&data) -} diff --git a/matrix/pushrules/action_test.go b/matrix/pushrules/action_test.go deleted file mode 100644 index 79b2fdf..0000000 --- a/matrix/pushrules/action_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/pushrules" -) - -func TestPushActionArray_Should_EmptyArrayReturnsDefaults(t *testing.T) { - should := pushrules.PushActionArray{}.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_MixedArrayReturnsExpected1(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.True(t, should.Notify) - assert.True(t, should.Highlight) - assert.True(t, should.PlaySound) - assert.Equal(t, "ping", should.SoundName) -} - -func TestPushActionArray_Should_MixedArrayReturnsExpected2(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: ""}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_NotifySet(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.True(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_NotifyAndCoalesceDoTheSameThing(t *testing.T) { - should1 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - }.Should() - should2 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - }.Should() - assert.Equal(t, should1, should2) -} - -func TestPushActionArray_Should_DontNotify(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - }.Should() - assert.True(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_HighlightBlank(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.True(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_HighlightFalse(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushActionArray_Should_SoundName(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.True(t, should.PlaySound) - assert.Equal(t, "ping", should.SoundName) -} - -func TestPushActionArray_Should_SoundNameEmpty(t *testing.T) { - should := pushrules.PushActionArray{ - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: ""}, - }.Should() - assert.False(t, should.NotifySpecified) - assert.False(t, should.Notify) - assert.False(t, should.Highlight) - assert.False(t, should.PlaySound) - assert.Empty(t, should.SoundName) -} - -func TestPushAction_UnmarshalJSON_InvalidJSONFails(t *testing.T) { - pa := &pushrules.PushAction{} - err := pa.UnmarshalJSON([]byte("Not JSON")) - assert.NotNil(t, err) -} - -func TestPushAction_UnmarshalJSON_InvalidTypeDoesNothing(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("unchanged"), - Tweak: pushrules.PushActionTweak("unchanged"), - Value: "unchanged", - } - - err := pa.UnmarshalJSON([]byte(`{"foo": "bar"}`)) - assert.Nil(t, err) - err = pa.UnmarshalJSON([]byte(`9001`)) - assert.Nil(t, err) - - assert.Equal(t, pushrules.PushActionType("unchanged"), pa.Action) - assert.Equal(t, pushrules.PushActionTweak("unchanged"), pa.Tweak) - assert.Equal(t, "unchanged", pa.Value) -} - -func TestPushAction_UnmarshalJSON_StringChangesActionType(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("unchanged"), - Tweak: pushrules.PushActionTweak("unchanged"), - Value: "unchanged", - } - - err := pa.UnmarshalJSON([]byte(`"foo"`)) - assert.Nil(t, err) - - assert.Equal(t, pushrules.PushActionType("foo"), pa.Action) - assert.Equal(t, pushrules.PushActionTweak("unchanged"), pa.Tweak) - assert.Equal(t, "unchanged", pa.Value) -} - -func TestPushAction_UnmarshalJSON_SetTweakChangesTweak(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("unchanged"), - Tweak: pushrules.PushActionTweak("unchanged"), - Value: "unchanged", - } - - err := pa.UnmarshalJSON([]byte(`{"set_tweak": "foo", "value": 123.0}`)) - assert.Nil(t, err) - - assert.Equal(t, pushrules.ActionSetTweak, pa.Action) - assert.Equal(t, pushrules.PushActionTweak("foo"), pa.Tweak) - assert.Equal(t, 123.0, pa.Value) -} - -func TestPushAction_MarshalJSON_TweakOutputWorks(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.ActionSetTweak, - Tweak: pushrules.PushActionTweak("foo"), - Value: "bar", - } - data, err := pa.MarshalJSON() - assert.Nil(t, err) - assert.Equal(t, []byte(`{"set_tweak":"foo","value":"bar"}`), data) -} - -func TestPushAction_MarshalJSON_OtherOutputWorks(t *testing.T) { - pa := &pushrules.PushAction{ - Action: pushrules.PushActionType("something else"), - Tweak: pushrules.PushActionTweak("foo"), - Value: "bar", - } - data, err := pa.MarshalJSON() - assert.Nil(t, err) - assert.Equal(t, []byte(`"something else"`), data) -} diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go deleted file mode 100644 index cc62da1..0000000 --- a/matrix/pushrules/condition.go +++ /dev/null @@ -1,162 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import ( - "regexp" - "strconv" - "strings" - "unicode" - - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" - - "maunium.net/go/gomuks/lib/glob" -) - -// Room is an interface with the functions that are needed for processing room-specific push conditions -type Room interface { - GetMember(mxid string) *rooms.Member - GetMembers() map[string]*rooms.Member - GetSessionOwner() string -} - -// PushCondKind is the type of a push condition. -type PushCondKind string - -// The allowed push condition kinds as specified in section 11.12.1.4.3 of r0.3.0 of the Client-Server API. -const ( - KindEventMatch PushCondKind = "event_match" - KindContainsDisplayName PushCondKind = "contains_display_name" - KindRoomMemberCount PushCondKind = "room_member_count" -) - -// PushCondition wraps a condition that is required for a specific PushRule to be used. -type PushCondition struct { - // The type of the condition. - Kind PushCondKind `json:"kind"` - // The dot-separated field of the event to match. Only applicable if kind is EventMatch. - Key string `json:"key,omitempty"` - // The glob-style pattern to match the field against. Only applicable if kind is EventMatch. - Pattern string `json:"pattern,omitempty"` - // The condition that needs to be fulfilled for RoomMemberCount-type conditions. - // A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found. - MemberCountCondition string `json:"is,omitempty"` -} - -// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions. -var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") - -// Match checks if this condition is fulfilled for the given event in the given room. -func (cond *PushCondition) Match(room Room, event *mautrix.Event) bool { - switch cond.Kind { - case KindEventMatch: - return cond.matchValue(room, event) - case KindContainsDisplayName: - return cond.matchDisplayName(room, event) - case KindRoomMemberCount: - return cond.matchMemberCount(room, event) - default: - return false - } -} - -func (cond *PushCondition) matchValue(room Room, event *mautrix.Event) bool { - index := strings.IndexRune(cond.Key, '.') - key := cond.Key - subkey := "" - if index > 0 { - subkey = key[index+1:] - key = key[0:index] - } - - pattern, err := glob.Compile(cond.Pattern) - if err != nil { - return false - } - - switch key { - case "type": - return pattern.MatchString(event.Type.String()) - case "sender": - return pattern.MatchString(event.Sender) - case "room_id": - return pattern.MatchString(event.RoomID) - case "state_key": - if event.StateKey == nil { - return cond.Pattern == "" - } - return pattern.MatchString(*event.StateKey) - case "content": - val, _ := event.Content.Raw[subkey].(string) - return pattern.MatchString(val) - default: - return false - } -} - -func (cond *PushCondition) matchDisplayName(room Room, event *mautrix.Event) bool { - ownerID := room.GetSessionOwner() - if ownerID == event.Sender { - return false - } - member := room.GetMember(ownerID) - if member == nil { - return false - } - - msg := event.Content.Body - isAcceptable := func(r uint8) bool { - return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r)) - } - length := len(member.Displayname) - for index := strings.Index(msg, member.Displayname); index != -1; index = strings.Index(msg, member.Displayname) { - if (index <= 0 || isAcceptable(msg[index-1])) && (index + length >= len(msg) || isAcceptable(msg[index+length])) { - return true - } - msg = msg[index+len(member.Displayname):] - } - return false -} - -func (cond *PushCondition) matchMemberCount(room Room, event *mautrix.Event) bool { - group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) - if len(group) != 3 { - return false - } - - operator := group[1] - wantedMemberCount, _ := strconv.Atoi(group[2]) - - memberCount := len(room.GetMembers()) - - switch operator { - case "==", "": - return memberCount == wantedMemberCount - case ">": - return memberCount > wantedMemberCount - case ">=": - return memberCount >= wantedMemberCount - case "<": - return memberCount < wantedMemberCount - case "<=": - return memberCount <= wantedMemberCount - default: - // Should be impossible due to regex. - return false - } -} diff --git a/matrix/pushrules/condition_displayname_test.go b/matrix/pushrules/condition_displayname_test.go deleted file mode 100644 index fd3f374..0000000 --- a/matrix/pushrules/condition_displayname_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "maunium.net/go/mautrix" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushCondition_Match_DisplayName(t *testing.T) { - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "tulir: test mention", - }) - event.Sender = "@someone_else:matrix.org" - assert.True(t, displaynamePushCondition.Match(displaynameTestRoom, event)) -} - -func TestPushCondition_Match_DisplayName_Fail(t *testing.T) { - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "not a mention", - }) - event.Sender = "@someone_else:matrix.org" - assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event)) -} - -func TestPushCondition_Match_DisplayName_CantHighlightSelf(t *testing.T) { - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "tulir: I can't highlight myself", - }) - assert.False(t, displaynamePushCondition.Match(displaynameTestRoom, event)) -} - -func TestPushCondition_Match_DisplayName_FailsOnEmptyRoom(t *testing.T) { - emptyRoom := newFakeRoom(0) - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "tulir: this room doesn't have the owner Member available, so it fails.", - }) - event.Sender = "@someone_else:matrix.org" - assert.False(t, displaynamePushCondition.Match(emptyRoom, event)) -} diff --git a/matrix/pushrules/condition_eventmatch_test.go b/matrix/pushrules/condition_eventmatch_test.go deleted file mode 100644 index e5761fc..0000000 --- a/matrix/pushrules/condition_eventmatch_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "maunium.net/go/mautrix" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushCondition_Match_KindEvent_MsgType(t *testing.T) { - condition := newMatchPushCondition("content.msgtype", "m.emote") - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.emote", - "body": "tests gomuks pushconditions", - }, - }) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_MsgType_Fail(t *testing.T) { - condition := newMatchPushCondition("content.msgtype", "m.emote") - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.text", - "body": "I'm testing gomuks pushconditions", - }, - }) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_EventType(t *testing.T) { - condition := newMatchPushCondition("type", "m.room.foo") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_EventType_IllegalGlob(t *testing.T) { - condition := newMatchPushCondition("type", "m.room.invalid_glo[b") - event := newFakeEvent(mautrix.NewEventType("m.room.invalid_glob"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_Sender_Fail(t *testing.T) { - condition := newMatchPushCondition("sender", "@foo:maunium.net") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_RoomID(t *testing.T) { - condition := newMatchPushCondition("room_id", "!fakeroom:maunium.net") - event := newFakeEvent(mautrix.NewEventType(""), mautrix.Content{}) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_BlankStateKey(t *testing.T) { - condition := newMatchPushCondition("state_key", "") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_BlankStateKey_Fail(t *testing.T) { - condition := newMatchPushCondition("state_key", "not blank") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_NonBlankStateKey(t *testing.T) { - condition := newMatchPushCondition("state_key", "*:maunium.net") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - event.StateKey = &event.Sender - assert.True(t, condition.Match(blankTestRoom, event)) -} - -func TestPushCondition_Match_KindEvent_UnknownKey(t *testing.T) { - condition := newMatchPushCondition("non-existent key", "doesn't affect anything") - event := newFakeEvent(mautrix.NewEventType("m.room.foo"), mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} diff --git a/matrix/pushrules/condition_membercount_test.go b/matrix/pushrules/condition_membercount_test.go deleted file mode 100644 index ad5da9f..0000000 --- a/matrix/pushrules/condition_membercount_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushCondition_Match_KindMemberCount_OneToOne_ImplicitPrefix(t *testing.T) { - condition := newCountPushCondition("2") - room := newFakeRoom(2) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_OneToOne_ExplicitPrefix(t *testing.T) { - condition := newCountPushCondition("==2") - room := newFakeRoom(2) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_BigRoom(t *testing.T) { - condition := newCountPushCondition(">200") - room := newFakeRoom(201) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_BigRoom_Fail(t *testing.T) { - condition := newCountPushCondition(">=200") - room := newFakeRoom(199) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_SmallRoom(t *testing.T) { - condition := newCountPushCondition("<10") - room := newFakeRoom(9) - assert.True(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_SmallRoom_Fail(t *testing.T) { - condition := newCountPushCondition("<=10") - room := newFakeRoom(11) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_InvalidPrefix(t *testing.T) { - condition := newCountPushCondition("??10") - room := newFakeRoom(11) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} - -func TestPushCondition_Match_KindMemberCount_InvalidCondition(t *testing.T) { - condition := newCountPushCondition("foobar") - room := newFakeRoom(1) - assert.False(t, condition.Match(room, countConditionTestEvent)) -} diff --git a/matrix/pushrules/condition_test.go b/matrix/pushrules/condition_test.go deleted file mode 100644 index 163c964..0000000 --- a/matrix/pushrules/condition_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/mautrix" - "maunium.net/go/gomuks/matrix/pushrules" - "maunium.net/go/gomuks/matrix/rooms" -) - -var ( - blankTestRoom *rooms.Room - displaynameTestRoom pushrules.Room - - countConditionTestEvent *mautrix.Event - - displaynamePushCondition *pushrules.PushCondition -) - -func init() { - blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net") - - countConditionTestEvent = &mautrix.Event{ - Sender: "@tulir:maunium.net", - Type: mautrix.EventMessage, - Timestamp: 1523791120, - ID: "$123:maunium.net", - RoomID: "!fakeroom:maunium.net", - Content: mautrix.Content{ - MsgType: mautrix.MsgText, - Body: "test", - }, - } - - displaynameTestRoom = newFakeRoom(4) - displaynamePushCondition = &pushrules.PushCondition{ - Kind: pushrules.KindContainsDisplayName, - } -} - -func newFakeEvent(evtType mautrix.EventType, content mautrix.Content) *mautrix.Event { - return &mautrix.Event{ - Sender: "@tulir:maunium.net", - Type: evtType, - Timestamp: 1523791120, - ID: "$123:maunium.net", - RoomID: "!fakeroom:maunium.net", - Content: content, - } -} - -func newCountPushCondition(condition string) *pushrules.PushCondition { - return &pushrules.PushCondition{ - Kind: pushrules.KindRoomMemberCount, - MemberCountCondition: condition, - } -} - -func newMatchPushCondition(key, pattern string) *pushrules.PushCondition { - return &pushrules.PushCondition{ - Kind: pushrules.KindEventMatch, - Key: key, - Pattern: pattern, - } -} - -func TestPushCondition_Match_InvalidKind(t *testing.T) { - condition := &pushrules.PushCondition{ - Kind: pushrules.PushCondKind("invalid"), - } - event := newFakeEvent(mautrix.EventType{Type: "m.room.foobar"}, mautrix.Content{}) - assert.False(t, condition.Match(blankTestRoom, event)) -} - -type FakeRoom struct { - members map[string]*mautrix.Member - owner string -} - -func newFakeRoom(memberCount int) *FakeRoom { - room := &FakeRoom{ - owner: "@tulir:maunium.net", - members: make(map[string]*mautrix.Member), - } - - if memberCount >= 1 { - room.members["@tulir:maunium.net"] = &mautrix.Member{ - Membership: mautrix.MembershipJoin, - Displayname: "tulir", - } - } - - for i := 0; i < memberCount-1; i++ { - mxid := fmt.Sprintf("@extrauser_%d:matrix.org", i) - room.members[mxid] = &mautrix.Member{ - Membership: mautrix.MembershipJoin, - Displayname: fmt.Sprintf("Extra User %d", i), - } - } - - return room -} - -func (fr *FakeRoom) GetMember(mxid string) *mautrix.Member { - return fr.members[mxid] -} - -func (fr *FakeRoom) GetSessionOwner() string { - return fr.owner -} - -func (fr *FakeRoom) GetMembers() map[string]*mautrix.Member { - return fr.members -} diff --git a/matrix/pushrules/doc.go b/matrix/pushrules/doc.go deleted file mode 100644 index 19cd774..0000000 --- a/matrix/pushrules/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pushrules contains utilities to parse push notification rules. -package pushrules diff --git a/matrix/pushrules/pushrules.go b/matrix/pushrules/pushrules.go deleted file mode 100644 index 643f2f2..0000000 --- a/matrix/pushrules/pushrules.go +++ /dev/null @@ -1,37 +0,0 @@ -package pushrules - -import ( - "encoding/json" - "net/url" - - "maunium.net/go/mautrix" -) - -// GetPushRules returns the push notification rules for the global scope. -func GetPushRules(client *mautrix.Client) (*PushRuleset, error) { - return GetScopedPushRules(client, "global") -} - -// GetScopedPushRules returns the push notification rules for the given scope. -func GetScopedPushRules(client *mautrix.Client, scope string) (resp *PushRuleset, err error) { - u, _ := url.Parse(client.BuildURL("pushrules", scope)) - // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. - u.Path += "/" - _, err = client.MakeRequest("GET", u.String(), nil, &resp) - return -} - -type contentWithRuleset struct { - Ruleset *PushRuleset `json:"global"` -} - -// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. -func EventToPushRules(event *mautrix.Event) (*PushRuleset, error) { - content := &contentWithRuleset{} - err := json.Unmarshal(event.Content.VeryRaw, content) - if err != nil { - return nil, err - } - - return content.Ruleset, nil -} diff --git a/matrix/pushrules/pushrules_test.go b/matrix/pushrules/pushrules_test.go deleted file mode 100644 index 1883c97..0000000 --- a/matrix/pushrules/pushrules_test.go +++ /dev/null @@ -1,249 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/mautrix" - "maunium.net/go/gomuks/matrix/pushrules" -) - -func TestEventToPushRules(t *testing.T) { - event := &mautrix.Event{ - Type: mautrix.AccountDataPushRules, - Timestamp: 1523380910, - Content: mautrix.Content{ - VeryRaw: json.RawMessage(JSONExamplePushRules), - }, - } - pushRuleset, err := pushrules.EventToPushRules(event) - assert.Nil(t, err) - assert.NotNil(t, pushRuleset) - - assert.IsType(t, pushRuleset.Override, pushrules.PushRuleArray{}) - assert.IsType(t, pushRuleset.Content, pushrules.PushRuleArray{}) - assert.IsType(t, pushRuleset.Room, pushrules.PushRuleMap{}) - assert.IsType(t, pushRuleset.Sender, pushrules.PushRuleMap{}) - assert.IsType(t, pushRuleset.Underride, pushrules.PushRuleArray{}) - assert.Len(t, pushRuleset.Override, 2) - assert.Len(t, pushRuleset.Content, 1) - assert.Empty(t, pushRuleset.Room.Map) - assert.Empty(t, pushRuleset.Sender.Map) - assert.Len(t, pushRuleset.Underride, 6) - - assert.Len(t, pushRuleset.Content[0].Actions, 3) - assert.True(t, pushRuleset.Content[0].Default) - assert.True(t, pushRuleset.Content[0].Enabled) - assert.Empty(t, pushRuleset.Content[0].Conditions) - assert.Equal(t, "alice", pushRuleset.Content[0].Pattern) - assert.Equal(t, ".m.rule.contains_user_name", pushRuleset.Content[0].RuleID) - - assert.False(t, pushRuleset.Override[0].Actions.Should().Notify) - assert.True(t, pushRuleset.Override[0].Actions.Should().NotifySpecified) -} - -const JSONExamplePushRules = `{ - "global": { - "content": [ - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight" - } - ], - "default": true, - "enabled": true, - "pattern": "alice", - "rule_id": ".m.rule.contains_user_name" - } - ], - "override": [ - { - "actions": [ - "dont_notify" - ], - "conditions": [], - "default": true, - "enabled": false, - "rule_id": ".m.rule.master" - }, - { - "actions": [ - "dont_notify" - ], - "conditions": [ - { - "key": "content.msgtype", - "kind": "event_match", - "pattern": "m.notice" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.suppress_notices" - } - ], - "room": [], - "sender": [], - "underride": [ - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "ring" - }, - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.call.invite" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.call" - }, - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight" - } - ], - "conditions": [ - { - "kind": "contains_display_name" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.contains_display_name" - }, - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "is": "2", - "kind": "room_member_count" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.room_one_to_one" - }, - { - "actions": [ - "notify", - { - "set_tweak": "sound", - "value": "default" - }, - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.room.member" - }, - { - "key": "content.membership", - "kind": "event_match", - "pattern": "invite" - }, - { - "key": "state_key", - "kind": "event_match", - "pattern": "@alice:example.com" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.invite_for_me" - }, - { - "actions": [ - "notify", - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.room.member" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.member_event" - }, - { - "actions": [ - "notify", - { - "set_tweak": "highlight", - "value": false - } - ], - "conditions": [ - { - "key": "type", - "kind": "event_match", - "pattern": "m.room.message" - } - ], - "default": true, - "enabled": true, - "rule_id": ".m.rule.message" - } - ] - } -}` diff --git a/matrix/pushrules/rule.go b/matrix/pushrules/rule.go deleted file mode 100644 index ef43721..0000000 --- a/matrix/pushrules/rule.go +++ /dev/null @@ -1,160 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import ( - "encoding/gob" - - "maunium.net/go/mautrix" - - "maunium.net/go/gomuks/lib/glob" -) - -func init() { - gob.Register(PushRuleArray{}) - gob.Register(PushRuleMap{}) -} - -type PushRuleCollection interface { - GetActions(room Room, event *mautrix.Event) PushActionArray -} - -type PushRuleArray []*PushRule - -func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray { - for _, rule := range rules { - rule.Type = typ - } - return rules -} - -func (rules PushRuleArray) GetActions(room Room, event *mautrix.Event) PushActionArray { - for _, rule := range rules { - if !rule.Match(room, event) { - continue - } - return rule.Actions - } - return nil -} - -type PushRuleMap struct { - Map map[string]*PushRule - Type PushRuleType -} - -func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap { - data := PushRuleMap{ - Map: make(map[string]*PushRule), - Type: typ, - } - for _, rule := range rules { - rule.Type = typ - data.Map[rule.RuleID] = rule - } - return data -} - -func (ruleMap PushRuleMap) GetActions(room Room, event *mautrix.Event) PushActionArray { - var rule *PushRule - var found bool - switch ruleMap.Type { - case RoomRule: - rule, found = ruleMap.Map[event.RoomID] - case SenderRule: - rule, found = ruleMap.Map[event.Sender] - } - if found && rule.Match(room, event) { - return rule.Actions - } - return nil -} - -func (ruleMap PushRuleMap) Unmap() PushRuleArray { - array := make(PushRuleArray, len(ruleMap.Map)) - index := 0 - for _, rule := range ruleMap.Map { - array[index] = rule - index++ - } - return array -} - -type PushRuleType string - -const ( - OverrideRule PushRuleType = "override" - ContentRule PushRuleType = "content" - RoomRule PushRuleType = "room" - SenderRule PushRuleType = "sender" - UnderrideRule PushRuleType = "underride" -) - -type PushRule struct { - // The type of this rule. - Type PushRuleType `json:"-"` - // The ID of this rule. - // For room-specific rules and user-specific rules, this is the room or user ID (respectively) - // For other types of rules, this doesn't affect anything. - RuleID string `json:"rule_id"` - // The actions this rule should trigger when matched. - Actions PushActionArray `json:"actions"` - // Whether this is a default rule, or has been set explicitly. - Default bool `json:"default"` - // Whether or not this push rule is enabled. - Enabled bool `json:"enabled"` - // The conditions to match in order to trigger this rule. - // Only applicable to generic underride/override rules. - Conditions []*PushCondition `json:"conditions,omitempty"` - // Pattern for content-specific push rules - Pattern string `json:"pattern,omitempty"` -} - -func (rule *PushRule) Match(room Room, event *mautrix.Event) bool { - if !rule.Enabled { - return false - } - switch rule.Type { - case OverrideRule, UnderrideRule: - return rule.matchConditions(room, event) - case ContentRule: - return rule.matchPattern(room, event) - case RoomRule: - return rule.RuleID == event.RoomID - case SenderRule: - return rule.RuleID == event.Sender - default: - return false - } -} - -func (rule *PushRule) matchConditions(room Room, event *mautrix.Event) bool { - for _, cond := range rule.Conditions { - if !cond.Match(room, event) { - return false - } - } - return true -} - -func (rule *PushRule) matchPattern(room Room, event *mautrix.Event) bool { - pattern, err := glob.Compile(rule.Pattern) - if err != nil { - return false - } - return pattern.MatchString(event.Content.Body) -} diff --git a/matrix/pushrules/rule_array_test.go b/matrix/pushrules/rule_array_test.go deleted file mode 100644 index 8bfc5e9..0000000 --- a/matrix/pushrules/rule_array_test.go +++ /dev/null @@ -1,294 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/pushrules" - "maunium.net/go/mautrix" - "testing" -) - -func TestPushRuleArray_GetActions_FirstMatchReturns(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "no match") - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!fakeroom:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@tulir:maunium.net", - Actions: actions3, - } - - rules := pushrules.PushRuleArray{rule1, rule2, rule3} - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Equal(t, rules.GetActions(blankTestRoom, event), actions2) -} - -func TestPushRuleArray_GetActions_NoMatchesIsNil(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "no match") - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "ping"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!realroom:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@otheruser:maunium.net", - Actions: actions3, - } - - rules := pushrules.PushRuleArray{rule1, rule2, rule3} - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Nil(t, rules.GetActions(blankTestRoom, event)) -} - -func TestPushRuleMap_GetActions_RoomRuleExists(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!realroom:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!thirdroom:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!fakeroom:maunium.net", - Actions: actions3, - } - - rules := pushrules.PushRuleMap{ - Map: map[string]*pushrules.PushRule{ - rule1.RuleID: rule1, - rule2.RuleID: rule2, - rule3.RuleID: rule3, - }, - Type: pushrules.RoomRule, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Equal(t, rules.GetActions(blankTestRoom, event), actions3) -} - -func TestPushRuleMap_GetActions_RoomRuleDoesntExist(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!realroom:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!thirdroom:maunium.net", - Actions: actions2, - } - - rules := pushrules.PushRuleMap{ - Map: map[string]*pushrules.PushRule{ - rule1.RuleID: rule1, - rule2.RuleID: rule2, - }, - Type: pushrules.RoomRule, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Nil(t, rules.GetActions(blankTestRoom, event)) -} - -func TestPushRuleMap_GetActions_SenderRuleExists(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@tulir:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@someone:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@otheruser:matrix.org", - Actions: actions3, - } - - rules := pushrules.PushRuleMap{ - Map: map[string]*pushrules.PushRule{ - rule1.RuleID: rule1, - rule2.RuleID: rule2, - rule3.RuleID: rule3, - }, - Type: pushrules.SenderRule, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.Equal(t, rules.GetActions(blankTestRoom, event), actions1) -} - -func TestPushRuleArray_SetTypeAndMap(t *testing.T) { - actions1 := pushrules.PushActionArray{ - {Action: pushrules.ActionDontNotify}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakHighlight, Value: false}, - {Action: pushrules.ActionSetTweak, Tweak: pushrules.TweakSound, Value: "pong"}, - } - rule1 := &pushrules.PushRule{ - Enabled: true, - RuleID: "@tulir:maunium.net", - Actions: actions1, - } - - actions2 := pushrules.PushActionArray{ - {Action: pushrules.ActionNotify}, - } - rule2 := &pushrules.PushRule{ - Enabled: true, - RuleID: "@someone:maunium.net", - Actions: actions2, - } - - actions3 := pushrules.PushActionArray{ - {Action: pushrules.ActionCoalesce}, - } - rule3 := &pushrules.PushRule{ - Enabled: true, - RuleID: "@otheruser:matrix.org", - Actions: actions3, - } - - ruleArray := pushrules.PushRuleArray{rule1, rule2, rule3} - ruleMap := ruleArray.SetTypeAndMap(pushrules.SenderRule) - assert.Equal(t, pushrules.SenderRule, ruleMap.Type) - for _, rule := range ruleArray { - assert.Equal(t, rule, ruleMap.Map[rule.RuleID]) - } - newRuleArray := ruleMap.Unmap() - for _, rule := range ruleArray { - assert.Contains(t, newRuleArray, rule) - } -} diff --git a/matrix/pushrules/rule_test.go b/matrix/pushrules/rule_test.go deleted file mode 100644 index 56d48fd..0000000 --- a/matrix/pushrules/rule_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules_test - -import ( - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/pushrules" - "maunium.net/go/mautrix" - "testing" -) - -func TestPushRule_Match_Conditions(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "*pushrules") - rule := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.emote", - "body": "is testing pushrules", - }, - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Conditions_Disabled(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "*pushrules") - rule := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: false, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.emote", - "body": "is testing pushrules", - }, - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Conditions_FailIfOneFails(t *testing.T) { - cond1 := newMatchPushCondition("content.msgtype", "m.emote") - cond2 := newMatchPushCondition("content.body", "*pushrules") - rule := &pushrules.PushRule{ - Type: pushrules.OverrideRule, - Enabled: true, - Conditions: []*pushrules.PushCondition{cond1, cond2}, - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - Raw: map[string]interface{}{ - "msgtype": "m.text", - "body": "I'm testing pushrules", - }, - MsgType: mautrix.MsgText, - Body: "I'm testing pushrules", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "is testing*", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is testing pushrules", - }) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content_Fail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "is testing*", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is not testing pushrules", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content_ImplicitGlob(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "testing", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "is not testing pushrules", - }) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Content_IllegalGlob(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.ContentRule, - Enabled: true, - Pattern: "this is not a valid glo[b", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{ - MsgType: mautrix.MsgEmote, - Body: "this is not a valid glob", - }) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Room(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!fakeroom:maunium.net", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Room_Fail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "!otherroom:maunium.net", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Sender(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.SenderRule, - Enabled: true, - RuleID: "@tulir:maunium.net", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.True(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_Sender_Fail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.RoomRule, - Enabled: true, - RuleID: "@someone:matrix.org", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.False(t, rule.Match(blankTestRoom, event)) -} - -func TestPushRule_Match_UnknownTypeAlwaysFail(t *testing.T) { - rule := &pushrules.PushRule{ - Type: pushrules.PushRuleType("foobar"), - Enabled: true, - RuleID: "@someone:matrix.org", - } - - event := newFakeEvent(mautrix.EventMessage, mautrix.Content{}) - assert.False(t, rule.Match(blankTestRoom, event)) -} diff --git a/matrix/pushrules/ruleset.go b/matrix/pushrules/ruleset.go deleted file mode 100644 index 7ad931a..0000000 --- a/matrix/pushrules/ruleset.go +++ /dev/null @@ -1,98 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package pushrules - -import ( - "encoding/json" - - "maunium.net/go/mautrix" -) - -type PushRuleset struct { - Override PushRuleArray - Content PushRuleArray - Room PushRuleMap - Sender PushRuleMap - Underride PushRuleArray -} - -type rawPushRuleset struct { - Override PushRuleArray `json:"override"` - Content PushRuleArray `json:"content"` - Room PushRuleArray `json:"room"` - Sender PushRuleArray `json:"sender"` - Underride PushRuleArray `json:"underride"` -} - -// UnmarshalJSON parses JSON into this PushRuleset. -// -// For override, sender and underride push rule arrays, the type is added -// to each PushRule and the array is used as-is. -// -// For room and sender push rule arrays, the type is added to each PushRule -// and the array is converted to a map with the rule ID as the key and the -// PushRule as the value. -func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) { - data := rawPushRuleset{} - err = json.Unmarshal(raw, &data) - if err != nil { - return - } - - rs.Override = data.Override.SetType(OverrideRule) - rs.Content = data.Content.SetType(ContentRule) - rs.Room = data.Room.SetTypeAndMap(RoomRule) - rs.Sender = data.Sender.SetTypeAndMap(SenderRule) - rs.Underride = data.Underride.SetType(UnderrideRule) - return -} - -// MarshalJSON is the reverse of UnmarshalJSON() -func (rs *PushRuleset) MarshalJSON() ([]byte, error) { - data := rawPushRuleset{ - Override: rs.Override, - Content: rs.Content, - Room: rs.Room.Unmap(), - Sender: rs.Sender.Unmap(), - Underride: rs.Underride, - } - return json.Marshal(&data) -} - -// DefaultPushActions is the value returned if none of the rule -// collections in a Ruleset match the event given to GetActions() -var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}} - -// GetActions matches the given event against all of the push rule -// collections in this push ruleset in the order of priority as -// specified in spec section 11.12.1.4. -func (rs *PushRuleset) GetActions(room Room, event *mautrix.Event) (match PushActionArray) { - // Add push rule collections to array in priority order - arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} - // Loop until one of the push rule collections matches the room/event combo. - for _, pra := range arrays { - if pra == nil { - continue - } - if match = pra.GetActions(room, event); match != nil { - // Match found, return it. - return - } - } - // No match found, return default actions. - return DefaultPushActions -} diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index fd7d53b..0c28a9d 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -27,6 +27,8 @@ import ( sync "github.com/sasha-s/go-deadlock" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/debug" ) @@ -54,25 +56,27 @@ type RoomTag struct { } type UnreadMessage struct { - EventID string + EventID id.EventID Counted bool Highlight bool } type Member struct { - mautrix.Member + event.Member // The user who sent the membership event - Sender string `json:"-"` + Sender id.UserID `json:"-"` } // Room represents a single Matrix room. type Room struct { // The room ID. - ID string + ID id.RoomID // Whether or not the user has left the room. HasLeft bool + // Whether or not the room is encrypted. + Encrypted bool // The first batch of events that has been fetched for this room. // Used for fetching additional history. @@ -80,14 +84,14 @@ type Room struct { // The last_batch field from the most recent sync. Used for fetching member lists. LastPrevBatch string // The MXID of the user whose session this room was created for. - SessionUserID string + SessionUserID id.UserID SessionMember *Member // The number of unread messages that were notified about. UnreadMessages []UnreadMessage unreadCountCache *int highlightCache *bool - lastMarkedRead string + lastMarkedRead id.EventID // Whether or not this room is marked as a direct chat. IsDirect bool @@ -101,10 +105,10 @@ type Room struct { // Whether or not the members for this room have been fetched from the server. MembersFetched bool // Room state cache. - state map[mautrix.EventType]map[string]*mautrix.Event + state map[event.Type]map[string]*event.Event // MXID -> Member cache calculated from membership events. - memberCache map[string]*Member - exMemberCache map[string]*Member + memberCache map[id.UserID]*Member + exMemberCache map[id.UserID]*Member // The first two non-SessionUserID members in the room. Calculated at // the same time as memberCache. firstMemberCache *Member @@ -117,11 +121,11 @@ type Room struct { // The topic of the room. Directly fetched from the m.room.topic state event. topicCache string // The canonical alias of the room. Directly fetched from the m.room.canonical_alias state event. - CanonicalAliasCache string + CanonicalAliasCache id.RoomAlias // Whether or not the room has been tombstoned. replacedCache bool // The room ID that replaced this room. - replacedByCache *string + replacedByCache *id.RoomID // Path for state store file. path string @@ -174,7 +178,7 @@ func (room *Room) load() { return } debug.Print("Loading state for room", room.ID, "from disk") - room.state = make(map[mautrix.EventType]map[string]*mautrix.Event) + room.state = make(map[event.Type]map[string]*event.Event) file, err := os.OpenFile(room.path, os.O_RDONLY, 0600) if err != nil { if !os.IsNotExist(err) { @@ -265,7 +269,7 @@ func (room *Room) Save() { } // MarkRead clears the new message statuses on this room. -func (room *Room) MarkRead(eventID string) bool { +func (room *Room) MarkRead(eventID id.EventID) bool { room.lock.Lock() defer room.lock.Unlock() if room.lastMarkedRead == eventID { @@ -319,7 +323,7 @@ func (room *Room) HasNewMessages() bool { return len(room.UnreadMessages) > 0 } -func (room *Room) AddUnread(eventID string, counted, highlight bool) { +func (room *Room) AddUnread(eventID id.EventID, counted, highlight bool) { room.lock.Lock() defer room.lock.Unlock() room.UnreadMessages = append(room.UnreadMessages, UnreadMessage{ @@ -341,18 +345,25 @@ func (room *Room) AddUnread(eventID string, counted, highlight bool) { } } +var ( + tagDirect = RoomTag{"net.maunium.gomuks.fake.direct", "0.5"} + tagInvite = RoomTag{"net.maunium.gomuks.fake.invite", "0.5"} + tagDefault = RoomTag{"", "0.5"} + tagLeave = RoomTag{"net.maunium.gomuks.fake.leave", "0.5"} +) + func (room *Room) Tags() []RoomTag { room.lock.RLock() defer room.lock.RUnlock() if len(room.RawTags) == 0 { if room.IsDirect { - return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}} - } else if room.SessionMember != nil && room.SessionMember.Membership == mautrix.MembershipInvite { - return []RoomTag{{"net.maunium.gomuks.fake.invite", "0.5"}} - } else if room.SessionMember != nil && room.SessionMember.Membership != mautrix.MembershipJoin { - return []RoomTag{{"net.maunium.gomuks.fake.leave", "0.5"}} + return []RoomTag{tagDirect} + } else if room.SessionMember != nil && room.SessionMember.Membership == event.MembershipInvite { + return []RoomTag{tagInvite} + } else if room.SessionMember != nil && room.SessionMember.Membership != event.MembershipJoin { + return []RoomTag{tagLeave} } - return []RoomTag{{"", "0.5"}} + return []RoomTag{tagDefault} } return room.RawTags } @@ -374,46 +385,46 @@ func (room *Room) UpdateSummary(summary mautrix.LazyLoadSummary) { // UpdateState updates the room's current state with the given Event. This will clobber events based // on the type/state_key combination. -func (room *Room) UpdateState(event *mautrix.Event) { - if event.StateKey == nil { +func (room *Room) UpdateState(evt *event.Event) { + if evt.StateKey == nil { panic("Tried to UpdateState() event with no state key.") } room.Load() room.lock.Lock() defer room.lock.Unlock() room.changed = true - _, exists := room.state[event.Type] + _, exists := room.state[evt.Type] if !exists { - room.state[event.Type] = make(map[string]*mautrix.Event) + room.state[evt.Type] = make(map[string]*event.Event) } - switch event.Type { - case mautrix.StateRoomName: - room.NameCache = event.Content.Name + switch evt.Type { + case event.StateRoomName: + room.NameCache = evt.Content.Name room.nameCacheSource = ExplicitRoomName - case mautrix.StateCanonicalAlias: + case event.StateCanonicalAlias: if room.nameCacheSource <= CanonicalAliasRoomName { - room.NameCache = event.Content.Alias + room.NameCache = string(evt.Content.Alias) room.nameCacheSource = CanonicalAliasRoomName } - room.CanonicalAliasCache = event.Content.Alias - case mautrix.StateMember: + room.CanonicalAliasCache = evt.Content.Alias + case event.StateMember: if room.nameCacheSource <= MemberRoomName { room.NameCache = "" } - room.updateMemberState(event) - case mautrix.StateTopic: - room.topicCache = event.Content.Topic + room.updateMemberState(evt) + case event.StateTopic: + room.topicCache = evt.Content.Topic } - if event.Type != mautrix.StateMember { - debug.Printf("Updating state %s#%s for %s", event.Type.String(), event.GetStateKey(), room.ID) + if evt.Type != event.StateMember { + debug.Printf("Updating state %s#%s for %s", evt.Type.String(), evt.GetStateKey(), room.ID) } - room.state[event.Type][*event.StateKey] = event + room.state[evt.Type][*evt.StateKey] = evt } -func (room *Room) updateMemberState(event *mautrix.Event) { - userID := event.GetStateKey() +func (room *Room) updateMemberState(event *event.Event) { + userID := id.UserID(event.GetStateKey()) if userID == room.SessionUserID { debug.Print("Updating session user state:", string(event.Content.VeryRaw)) room.SessionMember = room.eventToMember(userID, event.Sender, &event.Content) @@ -442,7 +453,7 @@ func (room *Room) updateMemberState(event *mautrix.Event) { } // GetStateEvent returns the state event for the given type/state_key combo, or nil. -func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event { +func (room *Room) GetStateEvent(eventType event.Type, stateKey string) *event.Event { room.Load() room.lock.RLock() defer room.lock.RUnlock() @@ -452,7 +463,7 @@ func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *m } // getStateEvents returns the state events for the given type. -func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event { +func (room *Room) getStateEvents(eventType event.Type) map[string]*event.Event { stateEventMap, _ := room.state[eventType] return stateEventMap } @@ -460,7 +471,7 @@ func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautri // GetTopic returns the topic of the room. func (room *Room) GetTopic() string { if len(room.topicCache) == 0 { - topicEvt := room.GetStateEvent(mautrix.StateTopic, "") + topicEvt := room.GetStateEvent(event.StateTopic, "") if topicEvt != nil { room.topicCache = topicEvt.Content.Topic } @@ -468,9 +479,9 @@ func (room *Room) GetTopic() string { return room.topicCache } -func (room *Room) GetCanonicalAlias() string { +func (room *Room) GetCanonicalAlias() id.RoomAlias { if len(room.CanonicalAliasCache) == 0 { - canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "") + canonicalAliasEvt := room.GetStateEvent(event.StateCanonicalAlias, "") if canonicalAliasEvt != nil { room.CanonicalAliasCache = canonicalAliasEvt.Content.Alias } else { @@ -485,7 +496,7 @@ func (room *Room) GetCanonicalAlias() string { // updateNameFromNameEvent updates the room display name to be the name set in the name event. func (room *Room) updateNameFromNameEvent() { - nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") + nameEvt := room.GetStateEvent(event.StateRoomName, "") if nameEvt != nil { room.NameCache = nameEvt.Content.Name } @@ -528,7 +539,7 @@ func (room *Room) updateNameCache() { room.nameCacheSource = ExplicitRoomName } if len(room.NameCache) == 0 { - room.NameCache = room.GetCanonicalAlias() + room.NameCache = string(room.GetCanonicalAlias()) room.nameCacheSource = CanonicalAliasRoomName } if len(room.NameCache) == 0 { @@ -548,8 +559,8 @@ func (room *Room) GetTitle() string { func (room *Room) IsReplaced() bool { if room.replacedByCache == nil { - evt := room.GetStateEvent(mautrix.StateTombstone, "") - var replacement string + evt := room.GetStateEvent(event.StateTombstone, "") + var replacement id.RoomID if evt != nil { replacement = evt.Content.ReplacementRoom } @@ -559,18 +570,18 @@ func (room *Room) IsReplaced() bool { return room.replacedCache } -func (room *Room) ReplacedBy() string { +func (room *Room) ReplacedBy() id.RoomID { if room.replacedByCache == nil { room.IsReplaced() } return *room.replacedByCache } -func (room *Room) eventToMember(userID string, sender string, content *mautrix.Content) *Member { +func (room *Room) eventToMember(userID, sender id.UserID, content *event.Content) *Member { member := content.Member member.Membership = content.Membership if len(member.Displayname) == 0 { - member.Displayname = userID + member.Displayname = string(userID) } return &Member{ Member: member, @@ -578,7 +589,7 @@ func (room *Room) eventToMember(userID string, sender string, content *mautrix.C } } -func (room *Room) updateNthMemberCache(userID string, member *Member) { +func (room *Room) updateNthMemberCache(userID id.UserID, member *Member) { if userID != room.SessionUserID { if room.firstMemberCache == nil { room.firstMemberCache = member @@ -589,19 +600,20 @@ func (room *Room) updateNthMemberCache(userID string, member *Member) { } // createMemberCache caches all member events into a easily processable MXID -> *Member map. -func (room *Room) createMemberCache() map[string]*Member { +func (room *Room) createMemberCache() map[id.UserID]*Member { if len(room.memberCache) > 0 { return room.memberCache } - cache := make(map[string]*Member) - exCache := make(map[string]*Member) + cache := make(map[id.UserID]*Member) + exCache := make(map[id.UserID]*Member) room.lock.RLock() - events := room.getStateEvents(mautrix.StateMember) + memberEvents := room.getStateEvents(event.StateMember) room.firstMemberCache = nil room.secondMemberCache = nil - if events != nil { - for userID, event := range events { - member := room.eventToMember(userID, event.Sender, &event.Content) + if memberEvents != nil { + for userIDStr, evt := range memberEvents { + userID := id.UserID(userIDStr) + member := room.eventToMember(userID, evt.Sender, &evt.Content) if member.Membership.IsInviteOrJoin() { cache[userID] = member room.updateNthMemberCache(userID, member) @@ -631,7 +643,7 @@ func (room *Room) createMemberCache() map[string]*Member { // // The members are returned from the cache. // If the cache is empty, it is updated first. -func (room *Room) GetMembers() map[string]*Member { +func (room *Room) GetMembers() map[id.UserID]*Member { room.Load() room.createMemberCache() return room.memberCache @@ -639,7 +651,7 @@ func (room *Room) GetMembers() map[string]*Member { // GetMember returns the member with the given MXID. // If the member doesn't exist, nil is returned. -func (room *Room) GetMember(userID string) *Member { +func (room *Room) GetMember(userID id.UserID) *Member { if userID == room.SessionUserID && room.SessionMember != nil { return room.SessionMember } @@ -660,16 +672,27 @@ func (room *Room) GetMember(userID string) *Member { return nil } +func (room *Room) GetMemberCount() int { + if room.memberCache == nil && room.Summary.JoinedMemberCount != nil { + return *room.Summary.JoinedMemberCount + } + return len(room.GetMembers()) +} + // GetSessionOwner returns the ID of the user whose session this room was created for. -func (room *Room) GetSessionOwner() string { - return room.SessionUserID +func (room *Room) GetOwnDisplayname() string { + member := room.GetMember(room.SessionUserID) + if member != nil { + return member.Displayname + } + return "" } // NewRoom creates a new Room with the given ID -func NewRoom(roomID string, cache *RoomCache) *Room { +func NewRoom(roomID id.RoomID, cache *RoomCache) *Room { return &Room{ ID: roomID, - state: make(map[mautrix.EventType]map[string]*mautrix.Event), + state: make(map[event.Type]map[string]*event.Event), path: cache.roomPath(roomID), cache: cache, diff --git a/matrix/rooms/room_test.go b/matrix/rooms/room_test.go deleted file mode 100644 index a1fc4a4..0000000 --- a/matrix/rooms/room_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package rooms_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" -) - -func TestNewRoom_DefaultValues(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - assert.Equal(t, "!test:maunium.net", room.ID) - assert.Equal(t, "@tulir:maunium.net", room.SessionUserID) - assert.Empty(t, room.GetMembers()) - assert.Equal(t, "Empty room", room.GetTitle()) - assert.Empty(t, room.GetAliases()) - assert.Empty(t, room.GetCanonicalAlias()) - assert.Empty(t, room.GetTopic()) - assert.Nil(t, room.GetMember(room.GetSessionOwner())) -} - -func TestRoom_GetCanonicalAlias(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateCanonicalAlias, - Content: mautrix.Content{ - Alias: "#foo:maunium.net", - }, - }) - assert.Equal(t, "#foo:maunium.net", room.GetCanonicalAlias()) -} - -func TestRoom_GetTopic(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateTopic, - Content: mautrix.Content{ - Topic: "test topic", - }, - }) - assert.Equal(t, "test topic", room.GetTopic()) -} - -func TestRoom_Tags_Empty(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - assert.Empty(t, room.RawTags) - tags := room.Tags() - assert.Len(t, tags, 1) - assert.Equal(t, "", tags[0].Tag) - assert.Equal(t, "0.5", tags[0].Order) -} - -func TestRoom_Tags_NotEmpty(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.RawTags = []rooms.RoomTag{{Tag: "foo", Order: "1"}, {Tag: "bar", Order: "1"}} - tags := room.Tags() - assert.Equal(t, room.RawTags, tags) -} - -func TestRoom_GetAliases(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addAliases(room) - - aliases := room.GetAliases() - assert.Contains(t, aliases, "#bar:maunium.net") - assert.Contains(t, aliases, "#test:maunium.net") - assert.Contains(t, aliases, "#foo:matrix.org") - assert.Contains(t, aliases, "#test:matrix.org") -} - -func addName(room *rooms.Room) { - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateRoomName, - Content: mautrix.Content{ - Name: "Test room", - }, - }) -} - -func addCanonicalAlias(room *rooms.Room) { - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateCanonicalAlias, - Content: mautrix.Content{ - Alias: "#foo:maunium.net", - }, - }) -} - -func addAliases(room *rooms.Room) { - server1 := "maunium.net" - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateAliases, - StateKey: &server1, - Content: mautrix.Content{ - Aliases: []string{"#bar:maunium.net", "#test:maunium.net", "#foo:maunium.net"}, - }, - }) - - server2 := "matrix.org" - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateAliases, - StateKey: &server2, - Content: mautrix.Content{ - Aliases: []string{"#foo:matrix.org", "#test:matrix.org"}, - }, - }) -} - -func addMembers(room *rooms.Room, count int) { - user1 := "@tulir:maunium.net" - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateMember, - StateKey: &user1, - Content: mautrix.Content{ - Member: mautrix.Member{ - Displayname: "tulir", - Membership: mautrix.MembershipJoin, - }, - }, - }) - - for i := 1; i < count; i++ { - userN := fmt.Sprintf("@user_%d:matrix.org", i+1) - content := mautrix.Content{ - Member: mautrix.Member{ - Membership: mautrix.MembershipJoin, - }, - } - if i%2 == 1 { - content.Displayname = fmt.Sprintf("User #%d", i+1) - } - if i%5 == 0 { - content.Membership = mautrix.MembershipInvite - } - room.UpdateState(&mautrix.Event{ - Type: mautrix.StateMember, - StateKey: &userN, - Content: content, - }) - } -} - -func TestRoom_GetMembers(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 6) - - members := room.GetMembers() - assert.Len(t, members, 6) -} - -func TestRoom_GetMember(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 6) - - assert.NotNil(t, room.GetMember("@user_2:matrix.org")) - assert.NotNil(t, room.GetMember("@tulir:maunium.net")) - assert.Equal(t, "@tulir:maunium.net", room.GetSessionOwner()) -} - -func TestRoom_GetTitle_ExplicitName(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 4) - addName(room) - addCanonicalAlias(room) - addAliases(room) - assert.Equal(t, "Test room", room.GetTitle()) -} - -func TestRoom_GetTitle_CanonicalAlias(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 4) - addCanonicalAlias(room) - addAliases(room) - assert.Equal(t, "#foo:maunium.net", room.GetTitle()) -} - -func TestRoom_GetTitle_FirstAlias(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 2) - addAliases(room) - assert.Equal(t, "#bar:maunium.net", room.GetTitle()) -} - -func TestRoom_GetTitle_Members_Empty(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 1) - assert.Equal(t, "Empty room", room.GetTitle()) -} - -func TestRoom_GetTitle_Members_OneToOne(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 2) - assert.Equal(t, "User #2", room.GetTitle()) -} - -func TestRoom_GetTitle_Members_GroupChat(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - addMembers(room, 76) - assert.Contains(t, room.GetTitle(), " and 74 others") -} - -func TestRoom_MarkRead(t *testing.T) { - room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - - room.AddUnread("foo", true, false) - assert.Equal(t, 1, room.UnreadCount()) - assert.False(t, room.Highlighted()) - - room.AddUnread("bar", true, false) - assert.Equal(t, 2, room.UnreadCount()) - assert.False(t, room.Highlighted()) - - room.AddUnread("asd", false, true) - assert.Equal(t, 2, room.UnreadCount()) - assert.True(t, room.Highlighted()) - - room.MarkRead("asd") - assert.Empty(t, room.UnreadMessages) -} diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go index 6fc400c..d442734 100644 --- a/matrix/rooms/roomcache.go +++ b/matrix/rooms/roomcache.go @@ -27,6 +27,7 @@ import ( sync "github.com/sasha-s/go-deadlock" "maunium.net/go/gomuks/debug" + "maunium.net/go/mautrix/id" ) // RoomCache contains room state info in a hashmap and linked list. @@ -37,15 +38,15 @@ type RoomCache struct { directory string maxSize int maxAge int64 - getOwner func() string + getOwner func() id.UserID - Map map[string]*Room + Map map[id.RoomID]*Room head *Room tail *Room size int } -func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() string) *RoomCache { +func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() id.UserID) *RoomCache { return &RoomCache{ listPath: listPath, directory: directory, @@ -53,7 +54,7 @@ func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwne maxAge: maxAge, getOwner: getOwner, - Map: make(map[string]*Room), + Map: make(map[id.RoomID]*Room), } } @@ -88,7 +89,7 @@ func (cache *RoomCache) LoadList() error { } // Read list - cache.Map = make(map[string]*Room, size) + cache.Map = make(map[id.RoomID]*Room, size) for i := 0; i < size; i++ { room := &Room{} err = dec.Decode(room) @@ -147,7 +148,7 @@ func (cache *RoomCache) SaveList() error { return nil } -func (cache *RoomCache) Touch(roomID string) { +func (cache *RoomCache) Touch(roomID id.RoomID) { cache.Lock() node, ok := cache.Map[roomID] if !ok || node == nil { @@ -174,14 +175,14 @@ func (cache *RoomCache) touch(node *Room) { node.touch = time.Now().Unix() } -func (cache *RoomCache) Get(roomID string) *Room { +func (cache *RoomCache) Get(roomID id.RoomID) *Room { cache.Lock() node := cache.get(roomID) cache.Unlock() return node } -func (cache *RoomCache) GetOrCreate(roomID string) *Room { +func (cache *RoomCache) GetOrCreate(roomID id.RoomID) *Room { cache.Lock() node := cache.get(roomID) if node == nil { @@ -192,7 +193,7 @@ func (cache *RoomCache) GetOrCreate(roomID string) *Room { return node } -func (cache *RoomCache) get(roomID string) *Room { +func (cache *RoomCache) get(roomID id.RoomID) *Room { node, ok := cache.Map[roomID] if ok && node != nil { return node @@ -215,11 +216,11 @@ func (cache *RoomCache) Put(room *Room) { node.Save() } -func (cache *RoomCache) roomPath(roomID string) string { - return filepath.Join(cache.directory, roomID+".gob.gz") +func (cache *RoomCache) roomPath(roomID id.RoomID) string { + return filepath.Join(cache.directory, string(roomID)+".gob.gz") } -func (cache *RoomCache) Load(roomID string) *Room { +func (cache *RoomCache) Load(roomID id.RoomID) *Room { cache.Lock() defer cache.Unlock() node, ok := cache.Map[roomID] @@ -312,7 +313,7 @@ func (cache *RoomCache) Unload(node *Room) { } } -func (cache *RoomCache) newRoom(roomID string) *Room { +func (cache *RoomCache) newRoom(roomID id.RoomID) *Room { node := NewRoom(roomID, cache) cache.Map[node.ID] = node return node diff --git a/matrix/sync.go b/matrix/sync.go index 8ec22b5..53c1798 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -24,14 +24,16 @@ import ( "time" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/rooms" ) type SyncerSession interface { - GetRoom(id string) *rooms.Room - GetUserID() string + GetRoom(id id.RoomID) *rooms.Room + GetUserID() id.UserID } type EventSource int @@ -45,6 +47,7 @@ const ( EventSourceTimeline EventSourceState EventSourceEphemeral + EventSourceToDevice ) func (es EventSource) String() string { @@ -83,14 +86,14 @@ func (es EventSource) String() string { return fmt.Sprintf("unknown (%d)", es) } -type EventHandler func(source EventSource, event *mautrix.Event) +type EventHandler func(source EventSource, event *event.Event) // GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively // replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer // pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information. type GomuksSyncer struct { Session SyncerSession - listeners map[mautrix.EventType][]EventHandler // event type to listeners array + listeners map[event.Type][]EventHandler // event type to listeners array FirstSyncDone bool InitDoneCallback func() } @@ -99,7 +102,7 @@ type GomuksSyncer struct { func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { return &GomuksSyncer{ Session: session, - listeners: make(map[mautrix.EventType][]EventHandler), + listeners: make(map[event.Type][]EventHandler), FirstSyncDone: false, } } @@ -152,33 +155,44 @@ func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err } func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []json.RawMessage, source EventSource) { - for _, event := range events { - if source == EventSourcePresence { - debug.Print(string(event)) - } - s.processSyncEvent(room, event, source) + for _, evt := range events { + s.processSyncEvent(room, evt, source) } } func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, eventJSON json.RawMessage, source EventSource) { - event := &mautrix.Event{} - err := json.Unmarshal(eventJSON, event) + evt := &event.Event{} + err := json.Unmarshal(eventJSON, evt) if err != nil { debug.Print("Failed to unmarshal event: %v\n%s", err, string(eventJSON)) return } + // Ensure the type class is correct. It's safe to mutate since it's not a pointer. + // Listeners are keyed by type structs, which means only the correct class will pass. + switch { + case evt.StateKey != nil: + evt.Type.Class = event.StateEventType + case source == EventSourcePresence, source & EventSourceEphemeral != 0: + evt.Type.Class = event.EphemeralEventType + case source & EventSourceAccountData != 0: + evt.Type.Class = event.AccountDataEventType + case source == EventSourceToDevice: + evt.Type.Class = event.ToDeviceEventType + default: + evt.Type.Class = event.MessageEventType + } if room != nil { - event.RoomID = room.ID - if source&EventSourceState != 0 || (source&EventSourceTimeline != 0 && event.Type.IsState() && event.StateKey != nil) { - room.UpdateState(event) + evt.RoomID = room.ID + if evt.Type.IsState() { + room.UpdateState(evt) } } - s.notifyListeners(source, event) + s.notifyListeners(source, evt) } // OnEventType allows callers to be notified when there are new events for the given event type. // There are no duplicate checks. -func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHandler) { +func (s *GomuksSyncer) OnEventType(eventType event.Type, callback EventHandler) { _, exists := s.listeners[eventType] if !exists { s.listeners[eventType] = []EventHandler{} @@ -186,21 +200,13 @@ func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHa s.listeners[eventType] = append(s.listeners[eventType], callback) } -func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) { - if (event.Type.IsState() && source&EventSourceState == 0 && event.StateKey == nil) || - (event.Type.IsAccountData() && source&EventSourceAccountData == 0) || - (event.Type.IsEphemeral() && event.Type != mautrix.EphemeralEventPresence && source&EventSourceEphemeral == 0) || - (event.Type == mautrix.EphemeralEventPresence && source&EventSourcePresence == 0) { - evtJson, _ := json.Marshal(event) - debug.Printf("Event of type %s received from mismatching source %s: %s", event.Type.String(), source.String(), string(evtJson)) - return - } - listeners, exists := s.listeners[event.Type] +func (s *GomuksSyncer) notifyListeners(source EventSource, evt *event.Event) { + listeners, exists := s.listeners[evt.Type] if !exists { return } for _, fn := range listeners { - fn(source, event) + fn(source, evt) } } @@ -211,53 +217,51 @@ func (s *GomuksSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Dura } // GetFilterJSON returns a filter with a timeline limit of 50. -func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { +func (s *GomuksSyncer) GetFilterJSON(_ id.UserID) json.RawMessage { filter := &mautrix.Filter{ Room: mautrix.RoomFilter{ IncludeLeave: false, State: mautrix.FilterPart{ LazyLoadMembers: true, - Types: []string{ - "m.room.member", - "m.room.name", - "m.room.topic", - "m.room.canonical_alias", - "m.room.aliases", - "m.room.power_levels", - "m.room.tombstone", + Types: []event.Type{ + event.StateMember, + event.StateRoomName, + event.StateTopic, + event.StateCanonicalAlias, + event.StatePowerLevels, + event.StateTombstone, }, }, Timeline: mautrix.FilterPart{ LazyLoadMembers: true, - Types: []string{ - "m.room.message", - "m.room.redaction", - "m.room.encrypted", - "m.sticker", - "m.reaction", + Types: []event.Type{ + event.EventMessage, + event.EventRedaction, + event.EventEncrypted, + event.EventSticker, + event.EventReaction, - "m.room.member", - "m.room.name", - "m.room.topic", - "m.room.canonical_alias", - "m.room.aliases", - "m.room.power_levels", - "m.room.tombstone", + event.StateMember, + event.StateRoomName, + event.StateTopic, + event.StateCanonicalAlias, + event.StatePowerLevels, + event.StateTombstone, }, -// Limit: 50, + Limit: 50, }, Ephemeral: mautrix.FilterPart{ - Types: []string{"m.typing", "m.receipt"}, + Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}, }, AccountData: mautrix.FilterPart{ - Types: []string{"m.tag"}, + Types: []event.Type{event.AccountDataRoomTags}, }, }, AccountData: mautrix.FilterPart{ - Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"}, + Types: []event.Type{event.AccountDataPushRules, event.AccountDataDirectChats, AccountDataGomuksPreferences}, }, Presence: mautrix.FilterPart{ - NotTypes: []string{"*"}, + NotTypes: []event.Type{event.NewEventType("*")}, }, } rawFilter, _ := json.Marshal(&filter) diff --git a/matrix/sync_test.go b/matrix/sync_test.go deleted file mode 100644 index 4b85a75..0000000 --- a/matrix/sync_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -package matrix_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "maunium.net/go/gomuks/matrix" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/mautrix" -) - -func TestGomuksSyncer_ProcessResponse_Initial(t *testing.T) { - syncer := matrix.NewGomuksSyncer(&mockSyncerSession{}) - var initDoneCalled = false - syncer.InitDoneCallback = func() { - initDoneCalled = true - } - - syncer.ProcessResponse(newRespSync(), "") - assert.True(t, syncer.FirstSyncDone) - assert.True(t, initDoneCalled) -} - -func TestGomuksSyncer_ProcessResponse(t *testing.T) { - mss := &mockSyncerSession{ - userID: "@tulir:maunium.net", - rooms: map[string]*rooms.Room{ - "!foo:maunium.net": { - Room: mautrix.NewRoom("!foo:maunium.net"), - }, - "!bar:maunium.net": { - Room: mautrix.NewRoom("!bar:maunium.net"), - }, - "!test:maunium.net": { - Room: mautrix.NewRoom("!test:maunium.net"), - }, - }, - } - ml := &mockListener{} - syncer := matrix.NewGomuksSyncer(mss) - syncer.OnEventType(mautrix.EventMessage, ml.receive) - syncer.OnEventType(mautrix.StateMember, ml.receive) - syncer.GetFilterJSON("@tulir:maunium.net") - - joinEvt := &mautrix.Event{ - ID: "!join:maunium.net", - Type: mautrix.StateMember, - Sender: "@tulir:maunium.net", - StateKey: ptr("̣@tulir:maunium.net"), - Content: mautrix.Content{ - Membership: mautrix.MembershipJoin, - }, - } - messageEvt := &mautrix.Event{ - ID: "!msg:maunium.net", - Type: mautrix.EventMessage, - Content: mautrix.Content{ - Body: "foo", - MsgType: mautrix.MsgText, - }, - } - unhandledEvt := &mautrix.Event{ - ID: "!unhandled:maunium.net", - Type: mautrix.EventType{Type: "m.room.unhandled_event"}, - } - inviteEvt := &mautrix.Event{ - ID: "!invite:matrix.org", - Type: mautrix.StateMember, - Sender: "@you:matrix.org", - StateKey: ptr("̣@tulir:maunium.net"), - Content: mautrix.Content{ - Membership: mautrix.MembershipInvite, - }, - } - leaveEvt := &mautrix.Event{ - ID: "!leave:matrix.org", - Type: mautrix.StateMember, - Sender: "@you:matrix.org", - StateKey: ptr("̣@tulir:maunium.net"), - Content: mautrix.Content{ - Membership: mautrix.MembershipLeave, - }, - } - - resp := newRespSync() - resp.Rooms.Join["!foo:maunium.net"] = join{ - State: events{Events: []*mautrix.Event{joinEvt}}, - Timeline: timeline{Events: []*mautrix.Event{messageEvt, unhandledEvt}}, - } - resp.Rooms.Invite["!bar:maunium.net"] = struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"invite_state"` - }{ - State: events{Events: []*mautrix.Event{inviteEvt}}, - } - resp.Rooms.Leave["!test:maunium.net"] = struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - }{ - State: events{Events: []*mautrix.Event{leaveEvt}}, - } - - syncer.ProcessResponse(resp, "since") - assert.Contains(t, ml.received, joinEvt, joinEvt.ID) - assert.Contains(t, ml.received, messageEvt, messageEvt.ID) - assert.NotContains(t, ml.received, unhandledEvt, unhandledEvt.ID) - assert.Contains(t, ml.received, inviteEvt, inviteEvt.ID) - assert.Contains(t, ml.received, leaveEvt, leaveEvt.ID) -} - -type mockSyncerSession struct { - rooms map[string]*rooms.Room - userID string -} - -func (mss *mockSyncerSession) GetRoom(id string) *rooms.Room { - return mss.rooms[id] -} - -func (mss *mockSyncerSession) GetUserID() string { - return mss.userID -} - -type events struct { - Events []*mautrix.Event `json:"events"` -} - -type timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` -} -type join struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - Ephemeral struct { - Events []*mautrix.Event `json:"events"` - } `json:"ephemeral"` - AccountData struct { - Events []*mautrix.Event `json:"events"` - } `json:"account_data"` -} - -func ptr(text string) *string { - return &text -} - -type mockListener struct { - received []*mautrix.Event -} - -func (ml *mockListener) receive(source matrix.EventSource, evt *mautrix.Event) { - ml.received = append(ml.received, evt) -} - -func newRespSync() *mautrix.RespSync { - resp := &mautrix.RespSync{NextBatch: "123"} - resp.Rooms.Join = make(map[string]struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - Ephemeral struct { - Events []*mautrix.Event `json:"events"` - } `json:"ephemeral"` - AccountData struct { - Events []*mautrix.Event `json:"events"` - } `json:"account_data"` - }) - resp.Rooms.Invite = make(map[string]struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"invite_state"` - }) - resp.Rooms.Leave = make(map[string]struct { - State struct { - Events []*mautrix.Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*mautrix.Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - }) - return resp -} |