diff --git a/appservice/account.go b/appservice/account.go index 2df2930..3791ee3 100644 --- a/appservice/account.go +++ b/appservice/account.go @@ -137,9 +137,13 @@ func (a *Account) userInfoUpdatedInternal(user UserID, info *UserInfo) error { } if info.Avatar != nil { - err2 := mx.ProfileAvatar(mx_user_id, info.Avatar) - if err2 != nil { - err = err2 + cache_key := fmt.Sprintf("%s/user_avatar/%s", a.Protocol, user) + cache_val := info.Avatar.Filename() + if cache_val == "" || dbCacheTestAndSet(cache_key, cache_val) { + err2 := mx.ProfileAvatar(mx_user_id, info.Avatar) + if err2 != nil { + err = err2 + } } } @@ -185,9 +189,13 @@ func (a *Account) roomInfoUpdatedInternal(roomId RoomID, author UserID, info *Ro } if info.Picture != nil { - err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid) - if err2 != nil { - err = err2 + cache_key := fmt.Sprintf("%s/room_picture/%s", a.Protocol, roomId) + cache_val := info.Picture.Filename() + if cache_val == "" || dbCacheTestAndSet(cache_key, cache_val) { + err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid) + if err2 != nil { + err = err2 + } } } @@ -204,6 +212,9 @@ func (a *Account) Event(event *Event) { } func (a *Account) eventInternal(event *Event) error { + // TODO: automatically ignore events that come from one of our bridged matrix users + // TODO: deduplicate events if we have several matrix users joined the same room (hard problem) + mx_user_id, err := dbGetMxUser(a.Protocol, event.Author) if err != nil { return err diff --git a/appservice/db.go b/appservice/db.go index 5cefd30..646dae3 100644 --- a/appservice/db.go +++ b/appservice/db.go @@ -23,6 +23,8 @@ func InitDb() error { return err } + db.AutoMigrate(&DbCache{}) + db.AutoMigrate(&DbUserMap{}) db.Model(&DbUserMap{}).AddIndex("idx_protocol_user", "protocol", "user_id") @@ -35,6 +37,14 @@ func InitDb() error { return nil } +// Long-term cache entries +type DbCache struct { + gorm.Model + + Key string `gorm:"unique_index"` + Value string +} + // User mapping between protocol user IDs and puppeted matrix ids type DbUserMap struct { gorm.Model @@ -76,6 +86,30 @@ type DbPmRoomMap struct { // ---- +func dbCacheGet(key string) string { + var entry DbCache + if db.Where(&DbCache{Key: key}).First(&entry).RecordNotFound() { + return "" + } else { + return entry.Value + } +} + +func dbCachePut(key string, value string) { + var entry DbCache + db.Where(&DbCache{Key: key}).Assign(&DbCache{Value: value}).FirstOrCreate(&entry) +} + +func dbCacheTestAndSet(key string, value string) bool { + // TODO make this really an atomic operation + // True if value was changed, false if was already set + if dbCacheGet(key) != value { + dbCachePut(key, value) + return true + } + return false +} + func dbGetMxRoom(protocol string, roomId connector.RoomID) (string, error) { var room DbRoomMap diff --git a/connector/mattermost/mattermost.go b/connector/mattermost/mattermost.go index 73ea66b..6282831 100644 --- a/connector/mattermost/mattermost.go +++ b/connector/mattermost/mattermost.go @@ -314,17 +314,21 @@ func (mm *Mattermost) handleConnected() { } else { room_info.Name = t.Team.Name + " / " + room_info.Name } - // TODO: cache last update time so we don't do this needlessly if t.Team.LastTeamIconUpdate > 0 { - team_img, resp := mm.conn.Client.GetTeamIcon(t.Id, "") - if resp.Error == nil { - room_info.Picture = &BlobMediaObject{ - ObjectFilename: t.Team.Name, - ObjectMimetype: http.DetectContentType(team_img), - ObjectData: team_img, - } - } else { - log.Warnf("Could not get team image: %s", resp.Error) + room_info.Picture = &LazyBlobMediaObject{ + ObjectFilename: fmt.Sprintf("%s-%d", + t.Team.Name, + t.Team.LastTeamIconUpdate), + GetFn: func(o *LazyBlobMediaObject) error { + team_img, resp := mm.conn.Client.GetTeamIcon(t.Id, "") + if resp.Error == nil { + log.Warnf("Could not get team image: %s", resp.Error) + return resp.Error + } + o.ObjectData = team_img + o.ObjectMimetype = http.DetectContentType(team_img) + return nil + }, } } break @@ -378,20 +382,24 @@ func (mm *Mattermost) updateUserInfo(user *model.User) { DisplayName: userDisp, } if user.LastPictureUpdate > 0 { - // TODO: cache last update time so we don't do this needlessly - img, resp := mm.conn.Client.GetProfileImage(user.Id, "") - if resp.Error == nil { - ui.Avatar = &BlobMediaObject{ - ObjectFilename: user.Username, - ObjectMimetype: http.DetectContentType(img), - ObjectData: img, - } - } else { - log.Warnf("Could not get profile picture: %s", resp.Error) + ui.Avatar = &LazyBlobMediaObject{ + ObjectFilename: fmt.Sprintf("%s-%d", + user.Username, + user.LastPictureUpdate), + GetFn: func(o *LazyBlobMediaObject) error { + img, resp := mm.conn.Client.GetProfileImage(user.Id, "") + if resp.Error == nil { + log.Warnf("Could not get profile picture: %s", resp.Error) + return resp.Error + } + o.ObjectData = img + o.ObjectMimetype = http.DetectContentType(img) + return nil + }, } - mm.handler.UserInfoUpdated(userId, ui) - mm.userdisplaynamemap[userId] = userDisp } + mm.handler.UserInfoUpdated(userId, ui) + mm.userdisplaynamemap[userId] = userDisp } } diff --git a/connector/mediaobject.go b/connector/mediaobject.go index a8d6f9a..244e571 100644 --- a/connector/mediaobject.go +++ b/connector/mediaobject.go @@ -132,3 +132,53 @@ type nullCloseReader struct { func (ncr nullCloseReader) Close() error { return nil } + +// ---- + +type LazyBlobMediaObject struct { + ObjectFilename string + ObjectMimetype string + ObjectImageSize *ImageSize + ObjectData []byte + + GetFn func(o *LazyBlobMediaObject) error +} + +func (m *LazyBlobMediaObject) Filename() string { + return m.ObjectFilename +} + +func (m *LazyBlobMediaObject) Size() int64 { + if m.ObjectData == nil { + m.GetFn(m) + } + return int64(len(m.ObjectData)) +} + +func (m *LazyBlobMediaObject) Mimetype() string { + if m.ObjectData == nil { + m.GetFn(m) + } + return m.ObjectMimetype +} + +func (m *LazyBlobMediaObject) ImageSize() *ImageSize { + if m.ObjectData == nil { + m.GetFn(m) + } + return m.ObjectImageSize +} + +func (m *LazyBlobMediaObject) Read() (io.ReadCloser, error) { + if m.ObjectData == nil { + err := m.GetFn(m) + if err != nil { + return nil, err + } + } + return nullCloseReader{bytes.NewBuffer(m.ObjectData)}, nil +} + +func (m *LazyBlobMediaObject) URL() string { + return "" +}