Move syncer/room store changes from gomatrix fork to here, refactor and improve stuff
This commit is contained in:
parent
b536064882
commit
0509b19562
2
debug.go
2
debug.go
@ -22,7 +22,7 @@ import (
|
||||
"maunium.net/go/tview"
|
||||
)
|
||||
|
||||
const DebugPaneHeight = 40
|
||||
const DebugPaneHeight = 35
|
||||
|
||||
type DebugPrinter interface {
|
||||
Printf(text string, args ...interface{})
|
||||
|
@ -126,5 +126,6 @@ func (gmx *gomuks) UI() *GomuksUI {
|
||||
}
|
||||
|
||||
func main() {
|
||||
NewGomuks(true).Start()
|
||||
debug := os.Getenv("DEBUG")
|
||||
NewGomuks(len(debug) > 0).Start()
|
||||
}
|
||||
|
85
matrix.go
85
matrix.go
@ -113,23 +113,29 @@ func (c *MatrixContainer) Stop() {
|
||||
func (c *MatrixContainer) UpdateRoomList() {
|
||||
rooms, err := c.client.JoinedRooms()
|
||||
if err != nil {
|
||||
c.debug.Print(err)
|
||||
c.debug.Print("Error fetching room list:", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.ui.MainView().SetRoomList(rooms.JoinedRooms)
|
||||
}
|
||||
|
||||
func (c *MatrixContainer) Start() {
|
||||
defer c.gmx.Recover()
|
||||
func (c *MatrixContainer) OnLogin() {
|
||||
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.member", c.HandleMembership)
|
||||
syncer.OnEventType("m.typing", c.HandleTyping)
|
||||
c.client.Syncer = syncer
|
||||
|
||||
c.UpdateRoomList()
|
||||
}
|
||||
|
||||
func (c *MatrixContainer) Start() {
|
||||
defer c.gmx.Recover()
|
||||
|
||||
c.OnLogin()
|
||||
|
||||
c.debug.Print("Starting sync...")
|
||||
c.running = true
|
||||
@ -151,65 +157,26 @@ func (c *MatrixContainer) Start() {
|
||||
}
|
||||
|
||||
func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) {
|
||||
message, _ := evt.Content["body"].(string)
|
||||
|
||||
room := c.ui.MainView().GetRoom(evt.RoomID)
|
||||
room, message := c.ui.MainView().ProcessMessageEvent(evt)
|
||||
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) {
|
||||
membership, _ := evt.Content["membership"].(string)
|
||||
if evt.StateKey != nil && *evt.StateKey == c.config.Session.MXID {
|
||||
prevMembership := "leave"
|
||||
if evt.Unsigned.PrevContent != nil {
|
||||
prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
|
||||
}
|
||||
if membership == prevMembership {
|
||||
const Hour = 1 * 60 * 60 * 1000
|
||||
if evt.Unsigned.Age > Hour {
|
||||
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
|
||||
room.room.UpdateState(evt)
|
||||
|
||||
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
|
||||
}
|
||||
// TODO This should probably also be in a different place
|
||||
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)
|
||||
err := c.client.StateEvent(roomID, "", "", &content)
|
||||
if err != nil {
|
||||
c.debug.Print(err)
|
||||
c.debug.Print("Error getting state of", roomID, err)
|
||||
return nil
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (c *MatrixContainer) GetRoom(roomID string) *gomatrix.Room {
|
||||
room := c.config.Session.LoadRoom(roomID)
|
||||
func (c *MatrixContainer) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) {
|
||||
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 {
|
||||
events := c.getState(room.ID)
|
||||
if events != nil {
|
||||
|
132
message-view.go
132
message-view.go
@ -17,6 +17,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@ -31,12 +32,23 @@ type Message struct {
|
||||
Sender string
|
||||
Text string
|
||||
Timestamp string
|
||||
RenderSender bool
|
||||
Date string
|
||||
|
||||
buffer []string
|
||||
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 (
|
||||
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
|
||||
spacePattern = regexp.MustCompile(`\s+`)
|
||||
@ -80,6 +92,7 @@ type MessageView struct {
|
||||
|
||||
ScrollOffset int
|
||||
MaxSenderWidth int
|
||||
DateFormat string
|
||||
TimestampFormat string
|
||||
TimestampWidth int
|
||||
Separator rune
|
||||
@ -92,6 +105,7 @@ type MessageView struct {
|
||||
lastDisplayMessage int
|
||||
totalHeight int
|
||||
|
||||
messageIDs map[string]bool
|
||||
messages []*Message
|
||||
}
|
||||
|
||||
@ -99,11 +113,15 @@ func NewMessageView() *MessageView {
|
||||
return &MessageView{
|
||||
Box: tview.NewBox(),
|
||||
MaxSenderWidth: 20,
|
||||
DateFormat: "January _2, 2006",
|
||||
TimestampFormat: "15:04:05",
|
||||
TimestampWidth: 8,
|
||||
Separator: '|',
|
||||
ScrollOffset: 0,
|
||||
|
||||
messages: make([]*Message, 0),
|
||||
messageIDs: make(map[string]bool),
|
||||
|
||||
widestSender: 5,
|
||||
prevWidth: -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
|
||||
if width != view.prevWidth {
|
||||
for _, message := range view.messages {
|
||||
message.calculateBuffer(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 {
|
||||
view.widestSender = len(sender)
|
||||
if 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),
|
||||
}
|
||||
|
||||
const (
|
||||
AppendMessage int = iota
|
||||
PrependMessage
|
||||
)
|
||||
|
||||
func (view *MessageView) AddMessage(message *Message, direction int) {
|
||||
_, messageExists := view.messageIDs[message.ID]
|
||||
if messageExists {
|
||||
return
|
||||
}
|
||||
_, _, width, height := view.GetInnerRect()
|
||||
|
||||
view.updateWidestSender(message.Sender)
|
||||
|
||||
_, _, width, _ := view.GetInnerRect()
|
||||
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
|
||||
message.calculateBuffer(width)
|
||||
|
||||
if direction == AppendMessage {
|
||||
if view.ScrollOffset > 0 {
|
||||
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.recalculateHeight(height)
|
||||
} else if direction == PrependMessage {
|
||||
view.messages = append([]*Message{message}, view.messages...)
|
||||
}
|
||||
|
||||
view.messageIDs[message.ID] = true
|
||||
view.recalculateHeight()
|
||||
}
|
||||
|
||||
func (view *MessageView) recalculateHeight(height int) {
|
||||
func (view *MessageView) recalculateHeight() {
|
||||
_, _, width, height := view.GetInnerRect()
|
||||
if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset {
|
||||
view.firstDisplayMessage = -1
|
||||
view.lastDisplayMessage = -1
|
||||
view.totalHeight = 0
|
||||
prevDate := ""
|
||||
for i := len(view.messages) - 1; i >= 0; i-- {
|
||||
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 {
|
||||
continue
|
||||
@ -170,6 +219,7 @@ func (view *MessageView) recalculateHeight(height int) {
|
||||
}
|
||||
}
|
||||
view.prevScrollOffset = view.ScrollOffset
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MessageView) PageUp() {
|
||||
@ -230,43 +280,67 @@ const (
|
||||
func (view *MessageView) Draw(screen tcell.Screen) {
|
||||
view.Box.Draw(screen)
|
||||
|
||||
x, y, width, height := view.GetInnerRect()
|
||||
if width != view.prevWidth {
|
||||
view.recalculateBuffers(width)
|
||||
}
|
||||
if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset {
|
||||
view.recalculateHeight(height)
|
||||
x, y, _, height := view.GetInnerRect()
|
||||
view.recalculateBuffers()
|
||||
view.recalculateHeight()
|
||||
|
||||
if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 {
|
||||
view.writeLine(screen, "It's quite empty in here.", x, y+height, tcell.ColorDefault)
|
||||
return
|
||||
}
|
||||
|
||||
usernameOffsetX := view.TimestampWidth + TimestampSenderGap
|
||||
messageOffsetX := usernameOffsetX + view.widestSender + SenderMessageGap
|
||||
|
||||
separatorX := x + usernameOffsetX + view.widestSender + SenderSeparatorGap
|
||||
for separatorY := y; separatorY < y+height; separatorY++ {
|
||||
screen.SetContent(separatorX, separatorY, view.Separator, nil, tcell.StyleDefault)
|
||||
}
|
||||
|
||||
if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
writeOffset := 0
|
||||
prevDate := ""
|
||||
prevSender := ""
|
||||
prevSenderLine := -1
|
||||
for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- {
|
||||
message := view.messages[i]
|
||||
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
|
||||
// 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 {
|
||||
senderAtLine = y
|
||||
}
|
||||
|
||||
view.writeLine(screen, message.Timestamp, x, senderAtLine, tcell.ColorDefault)
|
||||
if message.RenderSender || i == view.lastDisplayMessage {
|
||||
view.writeLineRight(screen, message.Sender,
|
||||
x+usernameOffsetX, senderAtLine,
|
||||
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 {
|
||||
offsetY := height - messageHeight - writeOffset + num
|
||||
// Only render message if it's within the message view.
|
||||
if offsetY >= 0 {
|
||||
view.writeLine(screen, line, x+messageOffsetX, y+offsetY, tcell.ColorDefault)
|
||||
}
|
||||
|
13
room-view.go
13
room-view.go
@ -24,7 +24,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"maunium.net/go/gomatrix"
|
||||
"maunium.net/go/tview"
|
||||
)
|
||||
|
||||
@ -35,7 +34,7 @@ type RoomView struct {
|
||||
content *MessageView
|
||||
status *tview.TextView
|
||||
userList *tview.TextView
|
||||
room *gomatrix.Room
|
||||
room *Room
|
||||
|
||||
parent *MainView
|
||||
}
|
||||
@ -52,7 +51,7 @@ func init() {
|
||||
sort.Sort(sort.StringSlice(colorNames))
|
||||
}
|
||||
|
||||
func NewRoomView(parent *MainView, room *gomatrix.Room) *RoomView {
|
||||
func NewRoomView(parent *MainView, room *Room) *RoomView {
|
||||
view := &RoomView{
|
||||
Box: tview.NewBox(),
|
||||
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)
|
||||
if member != nil {
|
||||
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()
|
||||
}
|
||||
|
175
room.go
Normal file
175
room.go
Normal 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),
|
||||
}
|
||||
}
|
28
session.go
28
session.go
@ -30,7 +30,7 @@ type Session struct {
|
||||
AccessToken string
|
||||
NextBatch string
|
||||
FilterID string
|
||||
Rooms map[string]*gomatrix.Room
|
||||
Rooms map[string]*Room
|
||||
|
||||
debug DebugPrinter `json:"-"`
|
||||
}
|
||||
@ -44,11 +44,18 @@ func (config *Config) NewSession(mxid string) *Session {
|
||||
return &Session{
|
||||
MXID: mxid,
|
||||
path: filepath.Join(config.dir, mxid+".session"),
|
||||
Rooms: make(map[string]*gomatrix.Room),
|
||||
Rooms: make(map[string]*Room),
|
||||
debug: config.debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Clear() {
|
||||
s.Rooms = make(map[string]*Room)
|
||||
s.NextBatch = ""
|
||||
s.FilterID = ""
|
||||
s.Save()
|
||||
}
|
||||
|
||||
func (s *Session) Load() {
|
||||
data, err := ioutil.ReadFile(s.path)
|
||||
if err != nil {
|
||||
@ -85,15 +92,20 @@ func (s *Session) LoadNextBatch(_ string) string {
|
||||
return s.NextBatch
|
||||
}
|
||||
|
||||
func (s *Session) LoadRoom(mxid string) *gomatrix.Room {
|
||||
func (s *Session) GetRoom(mxid string) *Room {
|
||||
room, _ := s.Rooms[mxid]
|
||||
if room == nil {
|
||||
room = gomatrix.NewRoom(mxid)
|
||||
s.SaveRoom(room)
|
||||
room = NewRoom(mxid)
|
||||
s.Rooms[room.ID] = room
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
func (s *Session) PutRoom(room *Room) {
|
||||
s.Rooms[room.ID] = room
|
||||
s.Save()
|
||||
}
|
||||
|
||||
func (s *Session) SaveFilterID(_, filterID string) {
|
||||
s.FilterID = filterID
|
||||
s.Save()
|
||||
@ -104,7 +116,11 @@ func (s *Session) SaveNextBatch(_, nextBatch string) {
|
||||
s.Save()
|
||||
}
|
||||
|
||||
func (s *Session) LoadRoom(mxid string) *gomatrix.Room {
|
||||
return s.GetRoom(mxid).Room
|
||||
}
|
||||
|
||||
func (s *Session) SaveRoom(room *gomatrix.Room) {
|
||||
s.Rooms[room.ID] = room
|
||||
s.GetRoom(room.ID).Room = room
|
||||
s.Save()
|
||||
}
|
||||
|
125
sync.go
Normal file
125
sync.go
Normal 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}}}`)
|
||||
}
|
112
view-main.go
112
view-main.go
@ -112,10 +112,6 @@ func findWordToTabComplete(text string) string {
|
||||
return output
|
||||
}
|
||||
|
||||
func (view *MainView) GetRoom(id string) *RoomView {
|
||||
return view.rooms[id]
|
||||
}
|
||||
|
||||
func (view *MainView) InputTabComplete(text string, cursorOffset int) string {
|
||||
roomView, _ := view.rooms[view.CurrentRoomID()]
|
||||
if roomView != nil {
|
||||
@ -156,10 +152,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
|
||||
case "/quit":
|
||||
view.gmx.Stop()
|
||||
case "/clearcache":
|
||||
view.config.Session.Rooms = make(map[string]*gomatrix.Room)
|
||||
view.config.Session.NextBatch = ""
|
||||
view.config.Session.FilterID = ""
|
||||
view.config.Session.Save()
|
||||
view.config.Session.Clear()
|
||||
view.gmx.Stop()
|
||||
case "/part":
|
||||
fallthrough
|
||||
@ -167,12 +160,12 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
|
||||
view.matrix.client.LeaveRoom(room)
|
||||
case "/join":
|
||||
if len(args) == 0 {
|
||||
view.AddMessage(room, "Usage: /join <room>")
|
||||
view.AddServiceMessage(room, "Usage: /join <room>")
|
||||
break
|
||||
}
|
||||
view.debug.Print(view.matrix.JoinRoom(args[0]))
|
||||
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.roomView.AddPage(room, roomView, true, false)
|
||||
roomView.UpdateUserList()
|
||||
view.GetHistory(room)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) GetRoom(id string) *RoomView {
|
||||
return view.rooms[id]
|
||||
}
|
||||
|
||||
func (view *MainView) HasRoom(room string) bool {
|
||||
for _, existingRoom := range view.roomIDs {
|
||||
if existingRoom == room {
|
||||
@ -263,6 +261,7 @@ func (view *MainView) RemoveRoom(room string) {
|
||||
view.roomList.RemoveItem(removeIndex)
|
||||
view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...)
|
||||
view.roomView.RemovePage(room)
|
||||
delete(view.rooms, room)
|
||||
view.Render()
|
||||
}
|
||||
|
||||
@ -270,6 +269,7 @@ func (view *MainView) SetRoomList(rooms []string) {
|
||||
view.roomIDs = rooms
|
||||
view.roomList.Clear()
|
||||
view.roomView.Clear()
|
||||
view.rooms = make(map[string]*RoomView)
|
||||
for index, room := range rooms {
|
||||
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]
|
||||
if ok {
|
||||
roomView.content.AddMessage("", "*", message, time.Now())
|
||||
messageView := roomView.MessageView()
|
||||
message := messageView.NewMessage("", "*", message, time.Now())
|
||||
messageView.AddMessage(message, AppendMessage)
|
||||
view.parent.Render()
|
||||
}
|
||||
}
|
||||
@ -295,3 +297,89 @@ func (view *MainView) AddMessage(room, message string) {
|
||||
func (view *MainView) 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user