Move syncer/room store changes from gomatrix fork to here, refactor and improve stuff

This commit is contained in:
Tulir Asokan 2018-03-18 17:34:42 +02:00
parent b536064882
commit 0509b19562
9 changed files with 602 additions and 145 deletions

View File

@ -22,7 +22,7 @@ import (
"maunium.net/go/tview" "maunium.net/go/tview"
) )
const DebugPaneHeight = 40 const DebugPaneHeight = 35
type DebugPrinter interface { type DebugPrinter interface {
Printf(text string, args ...interface{}) Printf(text string, args ...interface{})

View File

@ -126,5 +126,6 @@ func (gmx *gomuks) UI() *GomuksUI {
} }
func main() { func main() {
NewGomuks(true).Start() debug := os.Getenv("DEBUG")
NewGomuks(len(debug) > 0).Start()
} }

View File

@ -113,23 +113,29 @@ func (c *MatrixContainer) Stop() {
func (c *MatrixContainer) UpdateRoomList() { func (c *MatrixContainer) UpdateRoomList() {
rooms, err := c.client.JoinedRooms() rooms, err := c.client.JoinedRooms()
if err != nil { if err != nil {
c.debug.Print(err) c.debug.Print("Error fetching room list:", err)
return
} }
c.ui.MainView().SetRoomList(rooms.JoinedRooms) c.ui.MainView().SetRoomList(rooms.JoinedRooms)
} }
func (c *MatrixContainer) Start() { func (c *MatrixContainer) OnLogin() {
defer c.gmx.Recover()
c.client.Store = c.config.Session c.client.Store = c.config.Session
syncer := gomatrix.NewDefaultSyncer(c.config.Session.MXID, c.config.Session) syncer := NewGomuksSyncer(c.config.Session)
syncer.OnEventType("m.room.message", c.HandleMessage) syncer.OnEventType("m.room.message", c.HandleMessage)
syncer.OnEventType("m.room.member", c.HandleMembership) syncer.OnEventType("m.room.member", c.HandleMembership)
syncer.OnEventType("m.typing", c.HandleTyping) syncer.OnEventType("m.typing", c.HandleTyping)
c.client.Syncer = syncer c.client.Syncer = syncer
c.UpdateRoomList() c.UpdateRoomList()
}
func (c *MatrixContainer) Start() {
defer c.gmx.Recover()
c.OnLogin()
c.debug.Print("Starting sync...") c.debug.Print("Starting sync...")
c.running = true c.running = true
@ -151,65 +157,26 @@ func (c *MatrixContainer) Start() {
} }
func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) { func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) {
message, _ := evt.Content["body"].(string) room, message := c.ui.MainView().ProcessMessageEvent(evt)
room := c.ui.MainView().GetRoom(evt.RoomID)
if room != nil { if room != nil {
room.AddMessage(evt.ID, evt.Sender, message, unixToTime(evt.Timestamp)) room.AddMessage(message, AppendMessage)
} }
} }
func unixToTime(unix int64) time.Time {
timestamp := time.Now()
if unix != 0 {
timestamp = time.Unix(unix/1000, unix%1000*1000)
}
return timestamp
}
func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) { func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) {
membership, _ := evt.Content["membership"].(string) const Hour = 1 * 60 * 60 * 1000
if evt.StateKey != nil && *evt.StateKey == c.config.Session.MXID { if evt.Unsigned.Age > Hour {
prevMembership := "leave"
if evt.Unsigned.PrevContent != nil {
prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
}
if membership == prevMembership {
return return
} }
if membership == "join" {
c.ui.MainView().AddRoom(evt.RoomID)
} else if membership == "leave" {
c.ui.MainView().RemoveRoom(evt.RoomID)
}
return
}
room := c.ui.MainView().GetRoom(evt.RoomID)
room, message := c.ui.MainView().ProcessMembershipEvent(evt, true)
if room != nil {
// TODO this shouldn't be necessary // TODO this shouldn't be necessary
room.room.UpdateState(evt) room.room.UpdateState(evt)
// TODO This should probably also be in a different place
if room != nil {
var message, sender string
if membership == "invite" {
sender = "---"
message = fmt.Sprintf("%s invited %s.", evt.Sender, *evt.StateKey)
} else if membership == "join" {
sender = "-->"
message = fmt.Sprintf("%s joined the room.", *evt.StateKey)
} else if membership == "leave" {
sender = "<--"
if evt.Sender != *evt.StateKey {
reason, _ := evt.Content["reason"].(string)
message = fmt.Sprintf("%s kicked %s: %s", evt.Sender, *evt.StateKey, reason)
} else {
message = fmt.Sprintf("%s left the room.", *evt.StateKey)
}
} else {
return
}
room.UpdateUserList() room.UpdateUserList()
room.AddMessage(evt.ID, sender, message, unixToTime(evt.Timestamp))
room.AddMessage(message, AppendMessage)
} }
} }
@ -268,14 +235,22 @@ func (c *MatrixContainer) getState(roomID string) []*gomatrix.Event {
content := make([]*gomatrix.Event, 0) content := make([]*gomatrix.Event, 0)
err := c.client.StateEvent(roomID, "", "", &content) err := c.client.StateEvent(roomID, "", "", &content)
if err != nil { if err != nil {
c.debug.Print(err) c.debug.Print("Error getting state of", roomID, err)
return nil return nil
} }
return content return content
} }
func (c *MatrixContainer) GetRoom(roomID string) *gomatrix.Room { func (c *MatrixContainer) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) {
room := c.config.Session.LoadRoom(roomID) resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit)
if err != nil {
return nil, "", err
}
return resp.Chunk, resp.End, nil
}
func (c *MatrixContainer) GetRoom(roomID string) *Room {
room := c.config.Session.GetRoom(roomID)
if room != nil && len(room.State) == 0 { if room != nil && len(room.State) == 0 {
events := c.getState(room.ID) events := c.getState(room.ID)
if events != nil { if events != nil {

View File

@ -17,6 +17,7 @@
package main package main
import ( import (
"fmt"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -31,12 +32,23 @@ type Message struct {
Sender string Sender string
Text string Text string
Timestamp string Timestamp string
RenderSender bool Date string
buffer []string buffer []string
senderColor tcell.Color senderColor tcell.Color
} }
func NewMessage(id, sender, text, timestamp, date string, senderColor tcell.Color) *Message {
return &Message{
ID: id,
Sender: sender,
Text: text,
Timestamp: timestamp,
Date: date,
senderColor: senderColor,
}
}
var ( var (
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
spacePattern = regexp.MustCompile(`\s+`) spacePattern = regexp.MustCompile(`\s+`)
@ -80,6 +92,7 @@ type MessageView struct {
ScrollOffset int ScrollOffset int
MaxSenderWidth int MaxSenderWidth int
DateFormat string
TimestampFormat string TimestampFormat string
TimestampWidth int TimestampWidth int
Separator rune Separator rune
@ -92,6 +105,7 @@ type MessageView struct {
lastDisplayMessage int lastDisplayMessage int
totalHeight int totalHeight int
messageIDs map[string]bool
messages []*Message messages []*Message
} }
@ -99,11 +113,15 @@ func NewMessageView() *MessageView {
return &MessageView{ return &MessageView{
Box: tview.NewBox(), Box: tview.NewBox(),
MaxSenderWidth: 20, MaxSenderWidth: 20,
DateFormat: "January _2, 2006",
TimestampFormat: "15:04:05", TimestampFormat: "15:04:05",
TimestampWidth: 8, TimestampWidth: 8,
Separator: '|', Separator: '|',
ScrollOffset: 0, ScrollOffset: 0,
messages: make([]*Message, 0),
messageIDs: make(map[string]bool),
widestSender: 5, widestSender: 5,
prevWidth: -1, prevWidth: -1,
prevHeight: -1, prevHeight: -1,
@ -114,49 +132,80 @@ func NewMessageView() *MessageView {
} }
} }
func (view *MessageView) recalculateBuffers(width int) { func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *Message {
return NewMessage(id, sender, text,
timestamp.Format(view.TimestampFormat),
timestamp.Format(view.DateFormat),
getColor(sender))
}
func (view *MessageView) recalculateBuffers() {
_, _, width, _ := view.GetInnerRect()
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
if width != view.prevWidth {
for _, message := range view.messages { for _, message := range view.messages {
message.calculateBuffer(width) message.calculateBuffer(width)
} }
view.prevWidth = width view.prevWidth = width
} }
}
func (view *MessageView) AddMessage(id, sender, text string, timestamp time.Time) { func (view *MessageView) updateWidestSender(sender string) {
if len(sender) > view.widestSender { if len(sender) > view.widestSender {
view.widestSender = len(sender) view.widestSender = len(sender)
if view.widestSender > view.MaxSenderWidth { if view.widestSender > view.MaxSenderWidth {
view.widestSender = view.MaxSenderWidth view.widestSender = view.MaxSenderWidth
} }
} }
message := &Message{
ID: id,
Sender: sender,
RenderSender: true,
Text: text,
Timestamp: timestamp.Format(view.TimestampFormat),
senderColor: getColor(sender),
} }
_, _, width, height := view.GetInnerRect()
const (
AppendMessage int = iota
PrependMessage
)
func (view *MessageView) AddMessage(message *Message, direction int) {
_, messageExists := view.messageIDs[message.ID]
if messageExists {
return
}
view.updateWidestSender(message.Sender)
_, _, width, _ := view.GetInnerRect()
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
message.calculateBuffer(width) message.calculateBuffer(width)
if direction == AppendMessage {
if view.ScrollOffset > 0 { if view.ScrollOffset > 0 {
view.ScrollOffset += len(message.buffer) view.ScrollOffset += len(message.buffer)
} }
if len(view.messages) > 0 && view.messages[len(view.messages)-1].Sender == message.Sender {
message.RenderSender = false
}
view.messages = append(view.messages, message) view.messages = append(view.messages, message)
view.recalculateHeight(height) } else if direction == PrependMessage {
view.messages = append([]*Message{message}, view.messages...)
} }
func (view *MessageView) recalculateHeight(height int) { view.messageIDs[message.ID] = true
view.recalculateHeight()
}
func (view *MessageView) recalculateHeight() {
_, _, width, height := view.GetInnerRect()
if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset {
view.firstDisplayMessage = -1 view.firstDisplayMessage = -1
view.lastDisplayMessage = -1 view.lastDisplayMessage = -1
view.totalHeight = 0 view.totalHeight = 0
prevDate := ""
for i := len(view.messages) - 1; i >= 0; i-- { for i := len(view.messages) - 1; i >= 0; i-- {
prevTotalHeight := view.totalHeight prevTotalHeight := view.totalHeight
view.totalHeight += len(view.messages[i].buffer) message := view.messages[i]
view.totalHeight += len(message.buffer)
if message.Date != prevDate {
if len(prevDate) != 0 {
view.totalHeight++
}
prevDate = message.Date
}
if view.totalHeight < view.ScrollOffset { if view.totalHeight < view.ScrollOffset {
continue continue
@ -171,6 +220,7 @@ func (view *MessageView) recalculateHeight(height int) {
} }
view.prevScrollOffset = view.ScrollOffset view.prevScrollOffset = view.ScrollOffset
} }
}
func (view *MessageView) PageUp() { func (view *MessageView) PageUp() {
_, _, _, height := view.GetInnerRect() _, _, _, height := view.GetInnerRect()
@ -230,43 +280,67 @@ const (
func (view *MessageView) Draw(screen tcell.Screen) { func (view *MessageView) Draw(screen tcell.Screen) {
view.Box.Draw(screen) view.Box.Draw(screen)
x, y, width, height := view.GetInnerRect() x, y, _, height := view.GetInnerRect()
if width != view.prevWidth { view.recalculateBuffers()
view.recalculateBuffers(width) view.recalculateHeight()
}
if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset { if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 {
view.recalculateHeight(height) view.writeLine(screen, "It's quite empty in here.", x, y+height, tcell.ColorDefault)
return
} }
usernameOffsetX := view.TimestampWidth + TimestampSenderGap usernameOffsetX := view.TimestampWidth + TimestampSenderGap
messageOffsetX := usernameOffsetX + view.widestSender + SenderMessageGap messageOffsetX := usernameOffsetX + view.widestSender + SenderMessageGap
separatorX := x + usernameOffsetX + view.widestSender + SenderSeparatorGap separatorX := x + usernameOffsetX + view.widestSender + SenderSeparatorGap
for separatorY := y; separatorY < y+height; separatorY++ { for separatorY := y; separatorY < y+height; separatorY++ {
screen.SetContent(separatorX, separatorY, view.Separator, nil, tcell.StyleDefault) screen.SetContent(separatorX, separatorY, view.Separator, nil, tcell.StyleDefault)
} }
if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 {
return
}
writeOffset := 0 writeOffset := 0
prevDate := ""
prevSender := ""
prevSenderLine := -1
for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- { for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- {
message := view.messages[i] message := view.messages[i]
messageHeight := len(message.buffer) messageHeight := len(message.buffer)
// Show message when the date changes.
if message.Date != prevDate {
if len(prevDate) > 0 {
writeOffset++
view.writeLine(
screen, fmt.Sprintf("Date changed to %s", prevDate),
x+messageOffsetX, y+height-writeOffset, tcell.ColorGreen)
}
prevDate = message.Date
}
senderAtLine := y + height - writeOffset - messageHeight senderAtLine := y + height - writeOffset - messageHeight
// The message may be only partially on screen, so we need to make sure the sender
// is on screen even when the message is not shown completely.
if senderAtLine < y { if senderAtLine < y {
senderAtLine = y senderAtLine = y
} }
view.writeLine(screen, message.Timestamp, x, senderAtLine, tcell.ColorDefault) view.writeLine(screen, message.Timestamp, x, senderAtLine, tcell.ColorDefault)
if message.RenderSender || i == view.lastDisplayMessage {
view.writeLineRight(screen, message.Sender, view.writeLineRight(screen, message.Sender,
x+usernameOffsetX, senderAtLine, x+usernameOffsetX, senderAtLine,
view.widestSender, message.senderColor) view.widestSender, message.senderColor)
if message.Sender == prevSender {
// Sender is same as previous. We're looping from bottom to top, and we want the
// sender name only on the topmost message, so clear out the duplicate sender name
// below.
view.writeLineRight(screen, strings.Repeat(" ", view.widestSender),
x+usernameOffsetX, prevSenderLine,
view.widestSender, message.senderColor)
} }
prevSender = message.Sender
prevSenderLine = senderAtLine
for num, line := range message.buffer { for num, line := range message.buffer {
offsetY := height - messageHeight - writeOffset + num offsetY := height - messageHeight - writeOffset + num
// Only render message if it's within the message view.
if offsetY >= 0 { if offsetY >= 0 {
view.writeLine(screen, line, x+messageOffsetX, y+offsetY, tcell.ColorDefault) view.writeLine(screen, line, x+messageOffsetX, y+offsetY, tcell.ColorDefault)
} }

View File

@ -24,7 +24,6 @@ import (
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/gomatrix"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -35,7 +34,7 @@ type RoomView struct {
content *MessageView content *MessageView
status *tview.TextView status *tview.TextView
userList *tview.TextView userList *tview.TextView
room *gomatrix.Room room *Room
parent *MainView parent *MainView
} }
@ -52,7 +51,7 @@ func init() {
sort.Sort(sort.StringSlice(colorNames)) sort.Sort(sort.StringSlice(colorNames))
} }
func NewRoomView(parent *MainView, room *gomatrix.Room) *RoomView { func NewRoomView(parent *MainView, room *Room) *RoomView {
view := &RoomView{ view := &RoomView{
Box: tview.NewBox(), Box: tview.NewBox(),
topic: tview.NewTextView(), topic: tview.NewTextView(),
@ -166,11 +165,15 @@ func (view *RoomView) UpdateUserList() {
} }
} }
func (view *RoomView) AddMessage(id, sender, message string, timestamp time.Time) { func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) *Message {
member := view.room.GetMember(sender) member := view.room.GetMember(sender)
if member != nil { if member != nil {
sender = member.DisplayName sender = member.DisplayName
} }
view.content.AddMessage(id, sender, message, timestamp) return view.content.NewMessage(id, sender, text, timestamp)
}
func (view *RoomView) AddMessage(message *Message, direction int) {
view.content.AddMessage(message, direction)
view.parent.Render() view.parent.Render()
} }

175
room.go Normal file
View File

@ -0,0 +1,175 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"maunium.net/go/gomatrix"
)
// Room represents a single Matrix room.
type Room struct {
*gomatrix.Room
PrevBatch string
memberCache map[string]*RoomMember
nameCache string
topicCache string
}
// UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination.
func (room *Room) UpdateState(event *gomatrix.Event) {
_, exists := room.State[event.Type]
if !exists {
room.State[event.Type] = make(map[string]*gomatrix.Event)
}
switch event.Type {
case "m.room.member":
room.memberCache = nil
case "m.room.name":
case "m.room.canonical_alias":
case "m.room.alias":
room.nameCache = ""
case "m.room.topic":
room.topicCache = ""
}
room.State[event.Type][*event.StateKey] = event
}
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room *Room) GetStateEvent(eventType string, stateKey string) *gomatrix.Event {
stateEventMap, _ := room.State[eventType]
event, _ := stateEventMap[stateKey]
return event
}
// GetStateEvents returns the state events for the given type.
func (room *Room) GetStateEvents(eventType string) map[string]*gomatrix.Event {
stateEventMap, _ := room.State[eventType]
return stateEventMap
}
// GetTopic returns the topic of the room.
func (room *Room) GetTopic() string {
if len(room.topicCache) == 0 {
topicEvt := room.GetStateEvent("m.room.topic", "")
if topicEvt != nil {
room.topicCache, _ = topicEvt.Content["topic"].(string)
}
}
return room.topicCache
}
// GetTitle returns the display title of the room.
func (room *Room) GetTitle() string {
if len(room.nameCache) == 0 {
nameEvt := room.GetStateEvent("m.room.name", "")
if nameEvt != nil {
room.nameCache, _ = nameEvt.Content["name"].(string)
}
}
if len(room.nameCache) == 0 {
canonicalAliasEvt := room.GetStateEvent("m.room.canonical_alias", "")
if canonicalAliasEvt != nil {
room.nameCache, _ = canonicalAliasEvt.Content["alias"].(string)
}
}
if len(room.nameCache) == 0 {
// TODO the spec says clients should not use m.room.aliases for room names.
// However, Riot also uses m.room.aliases, so this is here now.
aliasEvents := room.GetStateEvents("m.room.aliases")
for _, event := range aliasEvents {
aliases, _ := event.Content["aliases"].([]interface{})
if len(aliases) > 0 {
room.nameCache, _ = aliases[0].(string)
break
}
}
}
if len(room.nameCache) == 0 {
// TODO follow other title rules in spec
room.nameCache = room.ID
}
return room.nameCache
}
type RoomMember struct {
UserID string `json:"-"`
Membership string `json:"membership"`
DisplayName string `json:"displayname"`
AvatarURL string `json:"avatar_url"`
}
func eventToRoomMember(userID string, event *gomatrix.Event) *RoomMember {
if event == nil {
return &RoomMember{
UserID: userID,
Membership: "leave",
}
}
membership, _ := event.Content["membership"].(string)
avatarURL, _ := event.Content["avatar_url"].(string)
displayName, _ := event.Content["displayname"].(string)
if len(displayName) == 0 {
displayName = userID
}
return &RoomMember{
UserID: userID,
Membership: membership,
DisplayName: displayName,
AvatarURL: avatarURL,
}
}
func (room *Room) createMemberCache() map[string]*RoomMember {
cache := make(map[string]*RoomMember)
events := room.GetStateEvents("m.room.member")
if events != nil {
for userID, event := range events {
member := eventToRoomMember(userID, event)
if member.Membership != "leave" {
cache[member.UserID] = member
}
}
}
room.memberCache = cache
return cache
}
func (room *Room) GetMembers() map[string]*RoomMember {
if len(room.memberCache) == 0 {
room.createMemberCache()
}
return room.memberCache
}
func (room *Room) GetMember(userID string) *RoomMember {
if len(room.memberCache) == 0 {
room.createMemberCache()
}
member, _ := room.memberCache[userID]
return member
}
// NewRoom creates a new Room with the given ID
func NewRoom(roomID string) *Room {
return &Room{
Room: gomatrix.NewRoom(roomID),
}
}

View File

@ -30,7 +30,7 @@ type Session struct {
AccessToken string AccessToken string
NextBatch string NextBatch string
FilterID string FilterID string
Rooms map[string]*gomatrix.Room Rooms map[string]*Room
debug DebugPrinter `json:"-"` debug DebugPrinter `json:"-"`
} }
@ -44,11 +44,18 @@ func (config *Config) NewSession(mxid string) *Session {
return &Session{ return &Session{
MXID: mxid, MXID: mxid,
path: filepath.Join(config.dir, mxid+".session"), path: filepath.Join(config.dir, mxid+".session"),
Rooms: make(map[string]*gomatrix.Room), Rooms: make(map[string]*Room),
debug: config.debug, debug: config.debug,
} }
} }
func (s *Session) Clear() {
s.Rooms = make(map[string]*Room)
s.NextBatch = ""
s.FilterID = ""
s.Save()
}
func (s *Session) Load() { func (s *Session) Load() {
data, err := ioutil.ReadFile(s.path) data, err := ioutil.ReadFile(s.path)
if err != nil { if err != nil {
@ -85,15 +92,20 @@ func (s *Session) LoadNextBatch(_ string) string {
return s.NextBatch return s.NextBatch
} }
func (s *Session) LoadRoom(mxid string) *gomatrix.Room { func (s *Session) GetRoom(mxid string) *Room {
room, _ := s.Rooms[mxid] room, _ := s.Rooms[mxid]
if room == nil { if room == nil {
room = gomatrix.NewRoom(mxid) room = NewRoom(mxid)
s.SaveRoom(room) s.Rooms[room.ID] = room
} }
return room return room
} }
func (s *Session) PutRoom(room *Room) {
s.Rooms[room.ID] = room
s.Save()
}
func (s *Session) SaveFilterID(_, filterID string) { func (s *Session) SaveFilterID(_, filterID string) {
s.FilterID = filterID s.FilterID = filterID
s.Save() s.Save()
@ -104,7 +116,11 @@ func (s *Session) SaveNextBatch(_, nextBatch string) {
s.Save() s.Save()
} }
func (s *Session) LoadRoom(mxid string) *gomatrix.Room {
return s.GetRoom(mxid).Room
}
func (s *Session) SaveRoom(room *gomatrix.Room) { func (s *Session) SaveRoom(room *gomatrix.Room) {
s.Rooms[room.ID] = room s.GetRoom(room.ID).Room = room
s.Save() s.Save()
} }

125
sync.go Normal file
View File

@ -0,0 +1,125 @@
package main
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
"maunium.net/go/gomatrix"
)
// GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively
// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
// pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information.
type GomuksSyncer struct {
Session *Session
listeners map[string][]gomatrix.OnEventListener // event type to listeners array
}
// NewGomuksSyncer returns an instantiated GomuksSyncer
func NewGomuksSyncer(session *Session) *GomuksSyncer {
return &GomuksSyncer{
Session: session,
listeners: make(map[string][]gomatrix.OnEventListener),
}
}
func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) {
if !s.shouldProcessResponse(res, since) {
return
}
// gdebug.Print("Processing sync response", since, res)
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.Session.MXID, since, r, debug.Stack())
}
}()
for _, event := range res.Presence.Events {
s.notifyListeners(&event)
}
for roomID, roomData := range res.Rooms.Join {
room := s.Session.GetRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
for _, event := range roomData.Timeline.Events {
event.RoomID = roomID
s.notifyListeners(&event)
}
for _, event := range roomData.Ephemeral.Events {
event.RoomID = roomID
s.notifyListeners(&event)
}
if len(room.PrevBatch) == 0 {
room.PrevBatch = roomData.Timeline.PrevBatch
}
}
for roomID, roomData := range res.Rooms.Invite {
room := s.Session.GetRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Leave {
room := s.Session.GetRoom(roomID)
for _, event := range roomData.Timeline.Events {
if event.StateKey != nil {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
if len(room.PrevBatch) == 0 {
room.PrevBatch = roomData.Timeline.PrevBatch
}
}
return
}
// OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks.
func (s *GomuksSyncer) OnEventType(eventType string, callback gomatrix.OnEventListener) {
_, exists := s.listeners[eventType]
if !exists {
s.listeners[eventType] = []gomatrix.OnEventListener{}
}
s.listeners[eventType] = append(s.listeners[eventType], callback)
}
// shouldProcessResponse returns true if the response should be processed. May modify the response to remove
// stuff that shouldn't be processed.
func (s *GomuksSyncer) shouldProcessResponse(resp *gomatrix.RespSync, since string) bool {
if since == "" {
return false
}
return true
}
func (s *GomuksSyncer) notifyListeners(event *gomatrix.Event) {
listeners, exists := s.listeners[event.Type]
if !exists {
return
}
for _, fn := range listeners {
fn(event)
}
}
// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
func (s *GomuksSyncer) OnFailedSync(res *gomatrix.RespSync, err error) (time.Duration, error) {
return 10 * time.Second, nil
}
// GetFilterJSON returns a filter with a timeline limit of 50.
func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`)
}

View File

@ -112,10 +112,6 @@ func findWordToTabComplete(text string) string {
return output return output
} }
func (view *MainView) GetRoom(id string) *RoomView {
return view.rooms[id]
}
func (view *MainView) InputTabComplete(text string, cursorOffset int) string { func (view *MainView) InputTabComplete(text string, cursorOffset int) string {
roomView, _ := view.rooms[view.CurrentRoomID()] roomView, _ := view.rooms[view.CurrentRoomID()]
if roomView != nil { if roomView != nil {
@ -156,10 +152,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
case "/quit": case "/quit":
view.gmx.Stop() view.gmx.Stop()
case "/clearcache": case "/clearcache":
view.config.Session.Rooms = make(map[string]*gomatrix.Room) view.config.Session.Clear()
view.config.Session.NextBatch = ""
view.config.Session.FilterID = ""
view.config.Session.Save()
view.gmx.Stop() view.gmx.Stop()
case "/part": case "/part":
fallthrough fallthrough
@ -167,12 +160,12 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
view.matrix.client.LeaveRoom(room) view.matrix.client.LeaveRoom(room)
case "/join": case "/join":
if len(args) == 0 { if len(args) == 0 {
view.AddMessage(room, "Usage: /join <room>") view.AddServiceMessage(room, "Usage: /join <room>")
break break
} }
view.debug.Print(view.matrix.JoinRoom(args[0])) view.debug.Print(view.matrix.JoinRoom(args[0]))
default: default:
view.AddMessage(room, "Unknown command.") view.AddServiceMessage(room, "Unknown command.")
} }
} }
@ -229,9 +222,14 @@ func (view *MainView) addRoom(index int, room string) {
view.rooms[room] = roomView view.rooms[room] = roomView
view.roomView.AddPage(room, roomView, true, false) view.roomView.AddPage(room, roomView, true, false)
roomView.UpdateUserList() roomView.UpdateUserList()
view.GetHistory(room)
} }
} }
func (view *MainView) GetRoom(id string) *RoomView {
return view.rooms[id]
}
func (view *MainView) HasRoom(room string) bool { func (view *MainView) HasRoom(room string) bool {
for _, existingRoom := range view.roomIDs { for _, existingRoom := range view.roomIDs {
if existingRoom == room { if existingRoom == room {
@ -263,6 +261,7 @@ func (view *MainView) RemoveRoom(room string) {
view.roomList.RemoveItem(removeIndex) view.roomList.RemoveItem(removeIndex)
view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...) view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...)
view.roomView.RemovePage(room) view.roomView.RemovePage(room)
delete(view.rooms, room)
view.Render() view.Render()
} }
@ -270,6 +269,7 @@ func (view *MainView) SetRoomList(rooms []string) {
view.roomIDs = rooms view.roomIDs = rooms
view.roomList.Clear() view.roomList.Clear()
view.roomView.Clear() view.roomView.Clear()
view.rooms = make(map[string]*RoomView)
for index, room := range rooms { for index, room := range rooms {
view.addRoom(index, room) view.addRoom(index, room)
} }
@ -284,10 +284,12 @@ func (view *MainView) SetTyping(room string, users []string) {
} }
} }
func (view *MainView) AddMessage(room, message string) { func (view *MainView) AddServiceMessage(room, message string) {
roomView, ok := view.rooms[room] roomView, ok := view.rooms[room]
if ok { if ok {
roomView.content.AddMessage("", "*", message, time.Now()) messageView := roomView.MessageView()
message := messageView.NewMessage("", "*", message, time.Now())
messageView.AddMessage(message, AppendMessage)
view.parent.Render() view.parent.Render()
} }
} }
@ -295,3 +297,89 @@ func (view *MainView) AddMessage(room, message string) {
func (view *MainView) Render() { func (view *MainView) Render() {
view.parent.Render() view.parent.Render()
} }
func (view *MainView) GetHistory(room string) {
roomView := view.rooms[room]
history, _, err := view.matrix.GetHistory(roomView.room.ID, view.config.Session.NextBatch, 50)
if err != nil {
view.debug.Print("Failed to fetch history for", roomView.room.ID, err)
return
}
for _, evt := range history {
var room *RoomView
var message *Message
if evt.Type == "m.room.message" {
room, message = view.ProcessMessageEvent(&evt)
} else if evt.Type == "m.room.member" {
room, message = view.ProcessMembershipEvent(&evt, false)
}
if room != nil && message != nil {
room.AddMessage(message, PrependMessage)
}
}
}
func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *RoomView, message *Message) {
room = view.GetRoom(evt.RoomID)
if room != nil {
text := evt.Content["body"].(string)
message = room.NewMessage(evt.ID, evt.Sender, text, unixToTime(evt.Timestamp))
}
return
}
func (view *MainView) processOwnMembershipChange(evt *gomatrix.Event) {
membership, _ := evt.Content["membership"].(string)
prevMembership := "leave"
if evt.Unsigned.PrevContent != nil {
prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
}
if membership == prevMembership {
return
}
if membership == "join" {
view.AddRoom(evt.RoomID)
} else if membership == "leave" {
view.RemoveRoom(evt.RoomID)
}
}
func (view *MainView) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (room *RoomView, message *Message) {
if new && evt.StateKey != nil && *evt.StateKey == view.config.Session.MXID {
view.processOwnMembershipChange(evt)
}
room = view.GetRoom(evt.RoomID)
if room != nil {
membership, _ := evt.Content["membership"].(string)
var sender, text string
if membership == "invite" {
sender = "---"
text = fmt.Sprintf("%s invited %s.", evt.Sender, *evt.StateKey)
} else if membership == "join" {
sender = "-->"
text = fmt.Sprintf("%s joined the room.", *evt.StateKey)
} else if membership == "leave" {
sender = "<--"
if evt.Sender != *evt.StateKey {
reason, _ := evt.Content["reason"].(string)
text = fmt.Sprintf("%s kicked %s: %s", evt.Sender, *evt.StateKey, reason)
} else {
text = fmt.Sprintf("%s left the room.", *evt.StateKey)
}
} else {
room = nil
return
}
message = room.NewMessage(evt.ID, sender, text, unixToTime(evt.Timestamp))
}
return
}
func unixToTime(unix int64) time.Time {
timestamp := time.Now()
if unix != 0 {
timestamp = time.Unix(unix/1000, unix%1000*1000)
}
return timestamp
}