morpheus

A Matrix client written in Go-QT
git clone git://archive.git.mtrnord.blog/Nordgedanken/morpheus.git
Log | Files | Refs | README | LICENSE

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 }