160 lines
4.6 KiB
Go
160 lines
4.6 KiB
Go
package bmatrix
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
type UserInRoomCacheEntry struct {
|
|
displayName *string
|
|
avatarURL *string
|
|
// for bridged messages that we sent, keep the source URL to know when to upgrade the
|
|
// profile picture (instead of doing it on every message)
|
|
sourceAvatar *string
|
|
lastUpdated time.Time
|
|
conflictWithOtherUsername bool
|
|
}
|
|
|
|
type UserCacheEntry struct {
|
|
globalEntry *UserInRoomCacheEntry
|
|
perChannel map[id.RoomID]UserInRoomCacheEntry
|
|
}
|
|
|
|
type UserInfoCache struct {
|
|
users map[id.UserID]UserCacheEntry
|
|
sync.RWMutex
|
|
}
|
|
|
|
func NewUserInfoCache() *UserInfoCache {
|
|
return &UserInfoCache{
|
|
users: make(map[id.UserID]UserCacheEntry),
|
|
RWMutex: sync.RWMutex{},
|
|
}
|
|
}
|
|
|
|
// note: the cache is read-locked inside this function
|
|
func (c *UserInfoCache) getAttributeFromCache(channelID id.RoomID, mxid id.UserID, attributeIsPresent func(UserInRoomCacheEntry) bool) *UserInRoomCacheEntry {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
|
|
if user, userPresent := c.users[mxid]; userPresent {
|
|
// try first the name of the user in the room, then globally
|
|
if roomCachedEntry, roomPresent := user.perChannel[channelID]; roomPresent && attributeIsPresent(roomCachedEntry) {
|
|
return &roomCachedEntry
|
|
}
|
|
|
|
if user.globalEntry != nil && attributeIsPresent(*user.globalEntry) {
|
|
return user.globalEntry
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// note: cache is locked inside this function
|
|
func (b *Bmatrix) cacheEntry(channelID id.RoomID, mxid id.UserID, callback func(UserInRoomCacheEntry) UserInRoomCacheEntry) {
|
|
now := time.Now()
|
|
|
|
cache := b.UserCache
|
|
|
|
cache.Lock()
|
|
defer cache.Unlock()
|
|
|
|
cache.clearObsoleteEntries(mxid)
|
|
|
|
var newEntry UserCacheEntry
|
|
if user, userPresent := cache.users[mxid]; userPresent {
|
|
newEntry = user
|
|
} else {
|
|
newEntry = UserCacheEntry{
|
|
globalEntry: nil,
|
|
perChannel: make(map[id.RoomID]UserInRoomCacheEntry),
|
|
}
|
|
}
|
|
|
|
cacheEntry := UserInRoomCacheEntry{
|
|
lastUpdated: now,
|
|
}
|
|
if channelID == "" && newEntry.globalEntry != nil {
|
|
cacheEntry = *newEntry.globalEntry
|
|
} else if channelID != "" {
|
|
if roomCachedEntry, roomPresent := newEntry.perChannel[channelID]; roomPresent {
|
|
cacheEntry = roomCachedEntry
|
|
}
|
|
}
|
|
|
|
newCacheEntry := callback(cacheEntry)
|
|
// try first the name of the user in the room, then globally
|
|
if channelID == "" {
|
|
newEntry.globalEntry = &newCacheEntry
|
|
} else {
|
|
// this is a local (room-specific) state, let's cache it as such
|
|
newEntry.perChannel[channelID] = newCacheEntry
|
|
}
|
|
|
|
cache.users[mxid] = newEntry
|
|
}
|
|
|
|
// scan to delete old entries, to stop memory usage from becoming high with obsolete entries.
|
|
// note: assume the cache is already write-locked
|
|
// TODO: should we update the timestamp when the entry is used?
|
|
func (c *UserInfoCache) clearObsoleteEntries(mxid id.UserID) {
|
|
// we have a "off-by-one" to account for when the user being added to the
|
|
// cache already have obsolete cache entries, as we want to keep it because
|
|
// we will be refreshing it in a minute
|
|
if len(c.users) <= MaxNumberOfUsersInCache+1 {
|
|
return
|
|
}
|
|
|
|
usersLastTimestamp := make(map[id.UserID]int64, len(c.users))
|
|
// compute the last updated timestamp entry for each user
|
|
for mxidIter, NicknameCacheIter := range c.users {
|
|
userLastTimestamp := time.Unix(0, 0)
|
|
for _, userInChannelCacheEntry := range NicknameCacheIter.perChannel {
|
|
if userInChannelCacheEntry.lastUpdated.After(userLastTimestamp) {
|
|
userLastTimestamp = userInChannelCacheEntry.lastUpdated
|
|
}
|
|
}
|
|
|
|
if NicknameCacheIter.globalEntry != nil {
|
|
if NicknameCacheIter.globalEntry.lastUpdated.After(userLastTimestamp) {
|
|
userLastTimestamp = NicknameCacheIter.globalEntry.lastUpdated
|
|
}
|
|
}
|
|
|
|
usersLastTimestamp[mxidIter] = userLastTimestamp.UnixNano()
|
|
}
|
|
|
|
// get the limit timestamp before which we must clear entries as obsolete
|
|
sortedTimestamps := make([]int64, 0, len(usersLastTimestamp))
|
|
for _, value := range usersLastTimestamp {
|
|
sortedTimestamps = append(sortedTimestamps, value)
|
|
}
|
|
sort.Slice(sortedTimestamps, func(i, j int) bool { return sortedTimestamps[i] < sortedTimestamps[j] })
|
|
limitTimestamp := sortedTimestamps[len(sortedTimestamps)-MaxNumberOfUsersInCache]
|
|
|
|
// delete entries older than the limit
|
|
for mxidIter, timestamp := range usersLastTimestamp {
|
|
// do not clear the user that we are adding to the cache
|
|
if timestamp <= limitTimestamp && mxidIter != mxid {
|
|
delete(c.users, mxidIter)
|
|
}
|
|
}
|
|
}
|
|
|
|
// note: cache is locked inside this function
|
|
func (c *UserInfoCache) removeFromCache(roomID id.RoomID, mxid id.UserID) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
if user, userPresent := c.users[mxid]; userPresent {
|
|
if _, roomPresent := user.perChannel[roomID]; roomPresent {
|
|
delete(user.perChannel, roomID)
|
|
c.users[mxid] = user
|
|
}
|
|
}
|
|
}
|