377 lines
8.3 KiB
Go
377 lines
8.3 KiB
Go
// gomuks - A terminal Matrix client written in Go.
|
|
// Copyright (C) 2020 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
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
sync "github.com/sasha-s/go-deadlock"
|
|
|
|
"maunium.net/go/mautrix/event"
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
"maunium.net/go/gomuks/debug"
|
|
)
|
|
|
|
// RoomCache contains room state info in a hashmap and linked list.
|
|
type RoomCache struct {
|
|
sync.Mutex
|
|
|
|
listPath string
|
|
directory string
|
|
maxSize int
|
|
maxAge int64
|
|
getOwner func() id.UserID
|
|
noUnload bool
|
|
|
|
Map map[id.RoomID]*Room
|
|
head *Room
|
|
tail *Room
|
|
size int
|
|
}
|
|
|
|
func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() id.UserID) *RoomCache {
|
|
return &RoomCache{
|
|
listPath: listPath,
|
|
directory: directory,
|
|
maxSize: maxSize,
|
|
maxAge: maxAge,
|
|
getOwner: getOwner,
|
|
|
|
Map: make(map[id.RoomID]*Room),
|
|
}
|
|
}
|
|
|
|
func (cache *RoomCache) DisableUnloading() {
|
|
cache.noUnload = true
|
|
}
|
|
|
|
func (cache *RoomCache) EnableUnloading() {
|
|
cache.noUnload = false
|
|
}
|
|
|
|
func (cache *RoomCache) IsEncrypted(roomID id.RoomID) bool {
|
|
room := cache.Get(roomID)
|
|
return room != nil && room.Encrypted
|
|
}
|
|
|
|
func (cache *RoomCache) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent {
|
|
room := cache.Get(roomID)
|
|
evt := room.GetStateEvent(event.StateEncryption, "")
|
|
if evt == nil {
|
|
return nil
|
|
}
|
|
content, ok := evt.Content.Parsed.(*event.EncryptionEventContent)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return content
|
|
}
|
|
|
|
func (cache *RoomCache) FindSharedRooms(userID id.UserID) (shared []id.RoomID) {
|
|
// FIXME this disables unloading so TouchNode wouldn't try to double-lock
|
|
cache.DisableUnloading()
|
|
cache.Lock()
|
|
for _, room := range cache.Map {
|
|
if !room.Encrypted {
|
|
continue
|
|
}
|
|
member, ok := room.GetMembers()[userID]
|
|
if ok && member.Membership == event.MembershipJoin {
|
|
shared = append(shared, room.ID)
|
|
}
|
|
}
|
|
cache.Unlock()
|
|
cache.EnableUnloading()
|
|
return
|
|
}
|
|
|
|
func (cache *RoomCache) LoadList() error {
|
|
cache.Lock()
|
|
defer cache.Unlock()
|
|
|
|
// Open room list file
|
|
file, err := os.OpenFile(cache.listPath, os.O_RDONLY, 0600)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to open room list file for reading: %w", err)
|
|
}
|
|
defer debugPrintError(file.Close, "Failed to close room list file after reading")
|
|
|
|
// Open gzip reader for room list file
|
|
cmpReader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read gzip room list: %w", err)
|
|
}
|
|
defer debugPrintError(cmpReader.Close, "Failed to close room list gzip reader")
|
|
|
|
// Open gob decoder for gzip reader
|
|
dec := gob.NewDecoder(cmpReader)
|
|
// Read number of items in list
|
|
var size int
|
|
err = dec.Decode(&size)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read size of room list: %w", err)
|
|
}
|
|
|
|
// Read list
|
|
cache.Map = make(map[id.RoomID]*Room, size)
|
|
for i := 0; i < size; i++ {
|
|
room := &Room{}
|
|
err = dec.Decode(room)
|
|
if err != nil {
|
|
debug.Printf("Failed to decode %dth room list entry: %v", i+1, err)
|
|
continue
|
|
}
|
|
room.path = cache.roomPath(room.ID)
|
|
room.cache = cache
|
|
cache.Map[room.ID] = room
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cache *RoomCache) SaveLoadedRooms() {
|
|
cache.Lock()
|
|
cache.clean(false)
|
|
for node := cache.head; node != nil; node = node.prev {
|
|
node.Save()
|
|
}
|
|
cache.Unlock()
|
|
}
|
|
|
|
func (cache *RoomCache) SaveList() error {
|
|
cache.Lock()
|
|
defer cache.Unlock()
|
|
|
|
debug.Print("Saving room list...")
|
|
// Open room list file
|
|
file, err := os.OpenFile(cache.listPath, os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open room list file for writing: %w", err)
|
|
}
|
|
defer debugPrintError(file.Close, "Failed to close room list file after writing")
|
|
|
|
// Open gzip writer for room list file
|
|
cmpWriter := gzip.NewWriter(file)
|
|
defer debugPrintError(cmpWriter.Close, "Failed to close room list gzip writer")
|
|
|
|
// Open gob encoder for gzip writer
|
|
enc := gob.NewEncoder(cmpWriter)
|
|
// Write number of items in list
|
|
err = enc.Encode(len(cache.Map))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write size of room list: %w", err)
|
|
}
|
|
|
|
// Write list
|
|
for _, node := range cache.Map {
|
|
err = enc.Encode(node)
|
|
if err != nil {
|
|
debug.Printf("Failed to encode room list entry of %s: %v", node.ID, err)
|
|
}
|
|
}
|
|
debug.Print("Room list saved to", cache.listPath, len(cache.Map), cache.size)
|
|
return nil
|
|
}
|
|
|
|
func (cache *RoomCache) Touch(roomID id.RoomID) {
|
|
cache.Lock()
|
|
node, ok := cache.Map[roomID]
|
|
if !ok || node == nil {
|
|
cache.Unlock()
|
|
return
|
|
}
|
|
cache.touch(node)
|
|
cache.Unlock()
|
|
}
|
|
|
|
func (cache *RoomCache) TouchNode(node *Room) {
|
|
if cache.noUnload || node.touch+2 > time.Now().Unix() {
|
|
return
|
|
}
|
|
cache.Lock()
|
|
cache.touch(node)
|
|
cache.Unlock()
|
|
}
|
|
|
|
func (cache *RoomCache) touch(node *Room) {
|
|
if node == cache.head {
|
|
return
|
|
}
|
|
debug.Print("Touching", node.ID)
|
|
cache.llPop(node)
|
|
cache.llPush(node)
|
|
node.touch = time.Now().Unix()
|
|
}
|
|
|
|
func (cache *RoomCache) Get(roomID id.RoomID) *Room {
|
|
cache.Lock()
|
|
node := cache.get(roomID)
|
|
cache.Unlock()
|
|
return node
|
|
}
|
|
|
|
func (cache *RoomCache) GetOrCreate(roomID id.RoomID) *Room {
|
|
cache.Lock()
|
|
node := cache.get(roomID)
|
|
if node == nil {
|
|
node = cache.newRoom(roomID)
|
|
cache.llPush(node)
|
|
}
|
|
cache.Unlock()
|
|
return node
|
|
}
|
|
|
|
func (cache *RoomCache) get(roomID id.RoomID) *Room {
|
|
node, ok := cache.Map[roomID]
|
|
if ok && node != nil {
|
|
return node
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cache *RoomCache) Put(room *Room) {
|
|
cache.Lock()
|
|
node := cache.get(room.ID)
|
|
if node != nil {
|
|
cache.touch(node)
|
|
} else {
|
|
cache.Map[room.ID] = room
|
|
if room.Loaded() {
|
|
cache.llPush(room)
|
|
}
|
|
node = room
|
|
}
|
|
cache.Unlock()
|
|
node.Save()
|
|
}
|
|
|
|
func (cache *RoomCache) roomPath(roomID id.RoomID) string {
|
|
escapedRoomID := strings.ReplaceAll(strings.ReplaceAll(string(roomID), "%", "%25"), "/", "%2F")
|
|
return filepath.Join(cache.directory, escapedRoomID+".gob.gz")
|
|
}
|
|
|
|
func (cache *RoomCache) Load(roomID id.RoomID) *Room {
|
|
cache.Lock()
|
|
defer cache.Unlock()
|
|
node, ok := cache.Map[roomID]
|
|
if ok {
|
|
return node
|
|
}
|
|
|
|
node = NewRoom(roomID, cache)
|
|
node.Load()
|
|
return node
|
|
}
|
|
|
|
func (cache *RoomCache) llPop(node *Room) {
|
|
if node.prev == nil && node.next == nil {
|
|
return
|
|
}
|
|
if node.prev != nil {
|
|
node.prev.next = node.next
|
|
}
|
|
if node.next != nil {
|
|
node.next.prev = node.prev
|
|
}
|
|
if node == cache.tail {
|
|
cache.tail = node.next
|
|
}
|
|
if node == cache.head {
|
|
cache.head = node.prev
|
|
}
|
|
node.next = nil
|
|
node.prev = nil
|
|
cache.size--
|
|
}
|
|
|
|
func (cache *RoomCache) llPush(node *Room) {
|
|
if node.next != nil || node.prev != nil {
|
|
debug.PrintStack()
|
|
debug.Print("Tried to llPush node that is already in stack")
|
|
return
|
|
}
|
|
if node == cache.head {
|
|
return
|
|
}
|
|
if cache.head != nil {
|
|
cache.head.next = node
|
|
}
|
|
node.prev = cache.head
|
|
node.next = nil
|
|
cache.head = node
|
|
if cache.tail == nil {
|
|
cache.tail = node
|
|
}
|
|
cache.size++
|
|
cache.clean(false)
|
|
}
|
|
|
|
func (cache *RoomCache) ForceClean() {
|
|
cache.Lock()
|
|
cache.clean(true)
|
|
cache.Unlock()
|
|
}
|
|
|
|
func (cache *RoomCache) clean(force bool) {
|
|
if cache.noUnload && !force {
|
|
return
|
|
}
|
|
origSize := cache.size
|
|
maxTS := time.Now().Unix() - cache.maxAge
|
|
for cache.size > cache.maxSize {
|
|
if cache.tail.touch > maxTS && !force {
|
|
break
|
|
}
|
|
ok := cache.tail.Unload()
|
|
node := cache.tail
|
|
cache.llPop(node)
|
|
if !ok {
|
|
debug.Print("Unload returned false, pushing node back")
|
|
cache.llPush(node)
|
|
}
|
|
}
|
|
if cleaned := origSize - cache.size; cleaned > 0 {
|
|
debug.Print("Cleaned", cleaned, "rooms")
|
|
}
|
|
}
|
|
|
|
func (cache *RoomCache) Unload(node *Room) {
|
|
cache.Lock()
|
|
defer cache.Unlock()
|
|
cache.llPop(node)
|
|
ok := node.Unload()
|
|
if !ok {
|
|
debug.Print("Unload returned false, pushing node back")
|
|
cache.llPush(node)
|
|
}
|
|
}
|
|
|
|
func (cache *RoomCache) newRoom(roomID id.RoomID) *Room {
|
|
node := NewRoom(roomID, cache)
|
|
cache.Map[node.ID] = node
|
|
return node
|
|
}
|