syncer.go (4800B)
1 package syncer 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "runtime/debug" 7 "time" 8 9 "github.com/matrix-org/gomatrix" 10 ) 11 12 type MorpheusSyncer struct { 13 UserID string 14 Store gomatrix.Storer 15 listeners map[string][]OnEventListener // event type to listeners array 16 } 17 18 // OnEventListener can be used with DefaultSyncer.OnEventType to be informed of incoming events. 19 type OnEventListener func(*gomatrix.Event) 20 21 // NewMorpheusSyncer returns an instantiated MorpheusSyncer 22 func NewMorpheusSyncer(userID string, store gomatrix.Storer) *MorpheusSyncer { 23 return &MorpheusSyncer{ 24 UserID: userID, 25 Store: store, 26 listeners: make(map[string][]OnEventListener), 27 } 28 } 29 30 // ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of 31 // unrepeating events. Returns a fatal error if a listener panics. 32 func (s *MorpheusSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) { 33 if !s.shouldProcessResponse(res, since) { 34 return 35 } 36 37 defer func() { 38 if r := recover(); r != nil { 39 err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.UserID, since, r, debug.Stack()) 40 } 41 }() 42 43 for roomID, roomData := range res.Rooms.Join { 44 room := s.getOrCreateRoom(roomID) 45 for _, event := range roomData.State.Events { 46 event.RoomID = roomID 47 room.UpdateState(&event) 48 s.notifyListeners(&event) 49 } 50 for _, event := range roomData.Timeline.Events { 51 event.RoomID = roomID 52 s.notifyListeners(&event) 53 } 54 } 55 for roomID, roomData := range res.Rooms.Invite { 56 room := s.getOrCreateRoom(roomID) 57 for _, event := range roomData.State.Events { 58 event.RoomID = roomID 59 room.UpdateState(&event) 60 s.notifyListeners(&event) 61 } 62 } 63 for roomID, roomData := range res.Rooms.Leave { 64 room := s.getOrCreateRoom(roomID) 65 for _, event := range roomData.Timeline.Events { 66 if event.StateKey != nil { 67 event.RoomID = roomID 68 room.UpdateState(&event) 69 s.notifyListeners(&event) 70 } 71 } 72 } 73 return 74 } 75 76 // OnEventType allows callers to be notified when there are new events for the given event type. 77 // There are no duplicate checks. 78 func (s *MorpheusSyncer) OnEventType(eventType string, callback OnEventListener) { 79 _, exists := s.listeners[eventType] 80 if !exists { 81 s.listeners[eventType] = []OnEventListener{} 82 } 83 s.listeners[eventType] = append(s.listeners[eventType], callback) 84 } 85 86 // shouldProcessResponse returns true if the response should be processed. May modify the response to remove 87 // stuff that shouldn't be processed. 88 func (s *MorpheusSyncer) shouldProcessResponse(resp *gomatrix.RespSync, since string) bool { 89 if since == "" { 90 return false 91 } 92 // This is a horrible hack because /sync will return the most recent messages for a room 93 // as soon as you /join it. We do NOT want to process those events in that particular room 94 // because they may have already been processed (if you toggle the bot in/out of the room). 95 // 96 // Work around this by inspecting each room's timeline and seeing if an m.room.member event for us 97 // exists and is "join" and then discard processing that room entirely if so. 98 // TODO: We probably want to process messages from after the last join event in the timeline. 99 for roomID, roomData := range resp.Rooms.Join { 100 for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- { 101 e := roomData.Timeline.Events[i] 102 if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID { 103 m := e.Content["membership"] 104 mship, ok := m.(string) 105 if !ok { 106 continue 107 } 108 if mship == "join" { 109 _, ok := resp.Rooms.Join[roomID] 110 if !ok { 111 continue 112 } 113 delete(resp.Rooms.Join, roomID) // don't re-process messages 114 delete(resp.Rooms.Invite, roomID) // don't re-process invites 115 break 116 } 117 } 118 } 119 } 120 return true 121 } 122 123 // getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse() 124 func (s *MorpheusSyncer) getOrCreateRoom(roomID string) *gomatrix.Room { 125 room := s.Store.LoadRoom(roomID) 126 if room == nil { // create a new Room 127 room = gomatrix.NewRoom(roomID) 128 s.Store.SaveRoom(room) 129 } 130 return room 131 } 132 133 func (s *MorpheusSyncer) notifyListeners(event *gomatrix.Event) { 134 listeners, exists := s.listeners[event.Type] 135 if !exists { 136 return 137 } 138 for _, fn := range listeners { 139 fn(event) 140 } 141 } 142 143 // OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error. 144 func (s *MorpheusSyncer) OnFailedSync(res *gomatrix.RespSync, err error) (time.Duration, error) { 145 return 10 * time.Second, nil 146 } 147 148 // GetFilterJSON returns a filter with a timeline limit of 50. 149 func (s *MorpheusSyncer) GetFilterJSON(userID string) json.RawMessage { 150 return json.RawMessage(`{"room":{"state":{"types":["m.room.*"]},"timeline":{"limit":20,"types":["m.room.message"]}}}`) 151 }