Organize files

This commit is contained in:
Tulir Asokan
2018-03-18 21:24:03 +02:00
parent 0509b19562
commit 72945c9a28
23 changed files with 620 additions and 392 deletions

284
matrix/matrix.go Normal file
View File

@ -0,0 +1,284 @@
// 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 matrix
import (
"fmt"
"strings"
"time"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface"
rooms "maunium.net/go/gomuks/matrix/room"
"maunium.net/go/gomuks/ui/debug"
"maunium.net/go/gomuks/ui/widget"
)
type Container struct {
client *gomatrix.Client
gmx ifc.Gomuks
ui ifc.GomuksUI
config *config.Config
running bool
stop chan bool
typing int64
}
func NewMatrixContainer(gmx ifc.Gomuks) *Container {
c := &Container{
config: gmx.Config(),
ui: gmx.UI(),
gmx: gmx,
}
return c
}
func (c *Container) InitClient() error {
if len(c.config.HS) == 0 {
return fmt.Errorf("no homeserver in config")
}
if c.client != nil {
c.Stop()
c.client = nil
}
var mxid, accessToken string
if c.config.Session != nil {
accessToken = c.config.Session.AccessToken
mxid = c.config.MXID
}
var err error
c.client, err = gomatrix.NewClient(c.config.HS, mxid, accessToken)
if err != nil {
return err
}
c.stop = make(chan bool, 1)
if c.config.Session != nil {
go c.Start()
}
return nil
}
func (c *Container) Initialized() bool {
return c.client != nil
}
func (c *Container) Login(user, password string) error {
resp, err := c.client.Login(&gomatrix.ReqLogin{
Type: "m.login.password",
User: user,
Password: password,
})
if err != nil {
return err
}
c.client.SetCredentials(resp.UserID, resp.AccessToken)
c.config.MXID = resp.UserID
c.config.Save()
c.config.Session = c.config.NewSession(resp.UserID)
c.config.Session.AccessToken = resp.AccessToken
c.config.Session.Save()
go c.Start()
return nil
}
func (c *Container) Stop() {
if c.running {
c.stop <- true
c.client.StopSync()
}
}
func (c *Container) Client() *gomatrix.Client {
return c.client
}
func (c *Container) UpdateRoomList() {
resp, err := c.client.JoinedRooms()
if err != nil {
debug.Print("Error fetching room list:", err)
return
}
c.ui.MainView().SetRooms(resp.JoinedRooms)
}
func (c *Container) OnLogin() {
c.client.Store = 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 *Container) Start() {
defer c.gmx.Recover()
c.ui.SetView(ifc.ViewMain)
c.OnLogin()
debug.Print("Starting sync...")
c.running = true
for {
select {
case <-c.stop:
debug.Print("Stopping sync...")
c.running = false
return
default:
if err := c.client.Sync(); err != nil {
debug.Print("Sync() errored", err)
} else {
debug.Print("Sync() returned without error")
}
}
}
}
func (c *Container) HandleMessage(evt *gomatrix.Event) {
room, message := c.ui.MainView().ProcessMessageEvent(evt)
if room != nil {
room.AddMessage(message, widget.AppendMessage)
}
}
func (c *Container) HandleMembership(evt *gomatrix.Event) {
const Hour = 1 * 60 * 60 * 1000
if evt.Unsigned.Age > Hour {
return
}
room, message := c.ui.MainView().ProcessMembershipEvent(evt, true)
if room != nil {
// TODO this shouldn't be necessary
room.Room.UpdateState(evt)
// TODO This should probably also be in a different place
room.UpdateUserList()
room.AddMessage(message, widget.AppendMessage)
}
}
func (c *Container) HandleTyping(evt *gomatrix.Event) {
users := evt.Content["user_ids"].([]interface{})
strUsers := make([]string, len(users))
for i, user := range users {
strUsers[i] = user.(string)
}
c.ui.MainView().SetTyping(evt.RoomID, strUsers)
}
func (c *Container) SendMessage(roomID, message string) {
c.gmx.Recover()
c.SendTyping(roomID, false)
c.client.SendText(roomID, message)
}
func (c *Container) SendTyping(roomID string, typing bool) {
c.gmx.Recover()
ts := time.Now().Unix()
if c.typing > ts && typing {
return
}
if typing {
c.client.UserTyping(roomID, true, 5000)
c.typing = ts + 5
} else {
c.client.UserTyping(roomID, false, 0)
c.typing = 0
}
}
func (c *Container) JoinRoom(roomID string) error {
if len(roomID) == 0 {
return fmt.Errorf("invalid room ID")
}
server := ""
if roomID[0] == '!' {
server = roomID[strings.Index(roomID, ":")+1:]
}
_, err := c.client.JoinRoom(roomID, server, nil)
if err != nil {
return err
}
// TODO probably safe to remove
// c.ui.MainView().AddRoom(resp.RoomID)
return nil
}
func (c *Container) LeaveRoom(roomID string) error {
if len(roomID) == 0 {
return fmt.Errorf("invalid room ID")
}
_, err := c.client.LeaveRoom(roomID)
if err != nil {
return err
}
return nil
}
func (c *Container) getState(roomID string) []*gomatrix.Event {
content := make([]*gomatrix.Event, 0)
err := c.client.StateEvent(roomID, "", "", &content)
if err != nil {
debug.Print("Error getting state of", roomID, err)
return nil
}
return content
}
func (c *Container) 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 *Container) GetRoom(roomID string) *rooms.Room {
room := c.config.Session.GetRoom(roomID)
if room != nil && len(room.State) == 0 {
events := c.getState(room.ID)
if events != nil {
for _, event := range events {
room.UpdateState(event)
}
}
}
return room
}

51
matrix/room/member.go Normal file
View File

@ -0,0 +1,51 @@
// 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 room
import (
"maunium.net/go/gomatrix"
)
type Member struct {
UserID string `json:"-"`
Membership string `json:"membership"`
DisplayName string `json:"displayname"`
AvatarURL string `json:"avatar_url"`
}
func eventToRoomMember(userID string, event *gomatrix.Event) *Member {
if event == nil {
return &Member{
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 &Member{
UserID: userID,
Membership: membership,
DisplayName: displayName,
AvatarURL: avatarURL,
}
}

145
matrix/room/room.go Normal file
View File

@ -0,0 +1,145 @@
// 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 room
import (
"maunium.net/go/gomatrix"
)
// Room represents a single Matrix room.
type Room struct {
*gomatrix.Room
PrevBatch string
memberCache map[string]*Member
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
}
func (room *Room) createMemberCache() map[string]*Member {
cache := make(map[string]*Member)
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]*Member {
if len(room.memberCache) == 0 {
room.createMemberCache()
}
return room.memberCache
}
func (room *Room) GetMember(userID string) *Member {
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),
}
}

126
matrix/sync.go Normal file
View File

@ -0,0 +1,126 @@
package matrix
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config"
)
// 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 *config.Session
listeners map[string][]gomatrix.OnEventListener // event type to listeners array
}
// NewGomuksSyncer returns an instantiated GomuksSyncer
func NewGomuksSyncer(session *config.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}}}`)
}