gomuks/matrix/matrix.go

1179 lines
32 KiB
Go
Raw Normal View History

// gomuks - A terminal Matrix client written in Go.
2020-04-19 17:10:14 +02:00
// Copyright (C) 2020 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
2019-01-17 13:13:25 +01:00
// 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
2019-01-17 13:13:25 +01:00
// GNU Affero General Public License for more details.
//
2019-01-17 13:13:25 +01:00
// 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/>.
2018-03-18 20:24:03 +01:00
package matrix
import (
"context"
2019-01-17 13:13:25 +01:00
"crypto/tls"
2020-04-19 14:00:49 +02:00
"encoding/gob"
2019-01-17 13:13:25 +01:00
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
2020-04-19 14:00:49 +02:00
"reflect"
2019-06-16 13:29:03 +02:00
"runtime"
dbg "runtime/debug"
"time"
"github.com/pkg/errors"
2019-01-17 13:13:25 +01:00
"maunium.net/go/mautrix"
2020-04-29 01:45:54 +02:00
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event"
2019-01-17 13:13:25 +01:00
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
2019-01-17 13:13:25 +01:00
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/open"
"maunium.net/go/gomuks/matrix/muksevt"
"maunium.net/go/gomuks/matrix/rooms"
)
2018-11-13 23:00:35 +01:00
// Container is a wrapper for a mautrix Client and some other stuff.
2018-03-21 22:29:58 +01:00
//
// It is used for all Matrix calls from the UI and Matrix event handlers.
2018-03-18 20:24:03 +01:00
type Container struct {
2018-11-13 23:00:35 +01:00
client *mautrix.Client
2020-05-05 19:38:58 +02:00
crypto CryptoInterface
syncer *GomuksSyncer
2018-03-18 20:24:03 +01:00
gmx ifc.Gomuks
ui ifc.GomuksUI
config *config.Config
history *HistoryManager
running bool
stop chan bool
2018-03-14 23:14:39 +01:00
typing int64
}
2018-03-21 22:29:58 +01:00
// NewContainer creates a new Container for the given Gomuks instance.
func NewContainer(gmx ifc.Gomuks) *Container {
2018-03-18 20:24:03 +01:00
c := &Container{
2018-03-13 20:58:43 +01:00
config: gmx.Config(),
ui: gmx.UI(),
gmx: gmx,
}
2018-03-13 20:58:43 +01:00
return c
}
2018-11-13 23:00:35 +01:00
// Client returns the underlying mautrix Client.
func (c *Container) Client() *mautrix.Client {
return c.client
}
2018-11-13 23:00:35 +01:00
type mxLogger struct{}
func (log mxLogger) Debugfln(message string, args ...interface{}) {
debug.Printf("[Matrix] "+message, args...)
}
2020-05-05 19:38:58 +02:00
type CryptoInterface interface {
Load() error
FlushStore() error
ProcessSyncResponse(resp *mautrix.RespSync, since string)
HandleMemberEvent(*event.Event)
DecryptMegolmEvent(*event.Event) (*event.Event, error)
EncryptMegolmEvent(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error)
ShareGroupSession(id.RoomID, []id.UserID) error
2020-04-26 23:38:04 +02:00
}
2018-11-13 23:00:35 +01:00
// InitClient initializes the mautrix client and connects to the homeserver specified in the config.
2018-03-18 20:24:03 +01:00
func (c *Container) InitClient() error {
if len(c.config.HS) == 0 {
return fmt.Errorf("no homeserver entered")
}
2018-03-13 20:58:43 +01:00
if c.client != nil {
c.Stop()
c.client = nil
2020-04-26 23:38:04 +02:00
c.crypto = nil
2018-03-13 20:58:43 +01:00
}
var mxid id.UserID
var accessToken string
if len(c.config.AccessToken) > 0 {
accessToken = c.config.AccessToken
2018-03-21 22:29:58 +01:00
mxid = c.config.UserID
}
var err error
2018-11-13 23:00:35 +01:00
c.client, err = mautrix.NewClient(c.config.HS, mxid, accessToken)
if err != nil {
return err
}
2018-11-13 23:00:35 +01:00
c.client.Logger = mxLogger{}
2020-04-26 23:38:04 +02:00
c.client.DeviceID = c.config.DeviceID
2020-05-05 19:38:58 +02:00
err = c.initCrypto()
2020-04-26 23:38:04 +02:00
if err != nil {
return err
}
if c.history == nil {
c.history, err = NewHistoryManager(c.config.HistoryPath)
if err != nil {
return errors.Wrap(err, "failed to initialize history")
}
}
allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0
if allowInsecure {
c.client.Client = &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
}
}
c.stop = make(chan bool, 1)
if len(accessToken) > 0 {
go c.Start()
}
return nil
}
2018-11-13 23:00:35 +01:00
// Initialized returns whether or not the mautrix client is initialized (see InitClient())
2018-03-18 20:24:03 +01:00
func (c *Container) Initialized() bool {
2018-03-13 20:58:43 +01:00
return c.client != nil
}
func (c *Container) PasswordLogin(user, password string) error {
2018-11-13 23:00:35 +01:00
resp, err := c.client.Login(&mautrix.ReqLogin{
2020-02-18 19:38:35 +01:00
Type: "m.login.password",
Identifier: mautrix.UserIdentifier{
Type: "m.id.user",
User: user,
},
2018-04-16 20:30:34 +02:00
Password: password,
InitialDeviceDisplayName: "gomuks",
})
if err != nil {
return err
}
c.finishLogin(resp)
return nil
}
func (c *Container) finishLogin(resp *mautrix.RespLogin) {
2018-03-13 20:58:43 +01:00
c.client.SetCredentials(resp.UserID, resp.AccessToken)
2020-04-26 23:38:04 +02:00
c.client.DeviceID = resp.DeviceID
2018-03-21 22:29:58 +01:00
c.config.UserID = resp.UserID
2020-04-26 23:38:04 +02:00
c.config.DeviceID = resp.DeviceID
c.config.AccessToken = resp.AccessToken
c.config.Save()
go c.Start()
}
func respondHTML(w http.ResponseWriter, status int, message string) {
w.Header().Add("Content-Type", "text/html")
w.WriteHeader(status)
_, _ = w.Write([]byte(fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<title>gomuks single-sign on</title>
<meta charset="utf-8"/>
</head>
<body>
<center>
<h2>%s</h2>
</center>
</body>
</html>`, message)))
}
func (c *Container) SingleSignOn() error {
loginURL := c.client.BuildURLWithQuery(mautrix.URLPath{"login", "sso", "redirect"}, map[string]string{
"redirectUrl": "http://localhost:29325",
})
err := open.Open(loginURL)
if err != nil {
return err
}
errChan := make(chan error, 1)
server := &http.Server{Addr: ":29325"}
server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
loginToken := r.URL.Query().Get("loginToken")
if len(loginToken) == 0 {
respondHTML(w, http.StatusBadRequest, "Missing loginToken parameter")
return
}
resp, err := c.client.Login(&mautrix.ReqLogin{
Type: "m.login.token",
Token: loginToken,
InitialDeviceDisplayName: "gomuks",
})
if err != nil {
respondHTML(w, http.StatusForbidden, err.Error())
errChan <- err
return
}
respondHTML(w, http.StatusOK, fmt.Sprintf("Successfully logged in as %s", resp.UserID))
c.finishLogin(resp)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = server.Shutdown(ctx)
if err != nil {
debug.Printf("Failed to shut down SSO server: %v\n", err)
}
errChan <- err
}()
})
err = server.ListenAndServe()
if err != nil {
return err
}
2020-02-19 00:14:02 +01:00
err = <-errChan
return err
}
// Login sends a password login request with the given username and password.
func (c *Container) Login(user, password string) error {
resp, err := c.client.GetLoginFlows()
if err != nil {
return err
}
if len(resp.Flows) == 1 && resp.Flows[0].Type == "m.login.password" {
return c.PasswordLogin(user, password)
} else if len(resp.Flows) == 2 && resp.Flows[0].Type == "m.login.sso" && resp.Flows[1].Type == "m.login.token" {
return c.SingleSignOn()
} else {
return fmt.Errorf("no supported login flows")
}
}
2018-05-10 14:47:24 +02:00
// Logout revokes the access token, stops the syncer and calls the OnLogout() method of the UI.
func (c *Container) Logout() {
c.client.Logout()
c.Stop()
c.config.DeleteSession()
2018-05-10 14:47:24 +02:00
c.client = nil
2020-04-26 23:38:04 +02:00
c.crypto = nil
2018-05-10 14:47:24 +02:00
c.ui.OnLogout()
}
2018-03-21 22:29:58 +01:00
// Stop stops the Matrix syncer.
2018-03-18 20:24:03 +01:00
func (c *Container) Stop() {
2018-03-15 20:28:21 +01:00
if c.running {
2018-04-24 15:51:40 +02:00
debug.Print("Stopping Matrix container...")
2018-03-15 20:28:21 +01:00
c.stop <- true
c.client.StopSync()
debug.Print("Closing history manager...")
err := c.history.Close()
if err != nil {
debug.Print("Error closing history manager:", err)
}
2020-02-19 22:48:34 +01:00
c.history = nil
2020-05-05 19:38:58 +02:00
if c.crypto != nil {
debug.Print("Flushing crypto store")
err = c.crypto.FlushStore()
if err != nil {
debug.Print("Error flushing crypto store:", err)
}
}
2018-03-15 20:28:21 +01:00
}
2018-03-13 20:58:43 +01:00
}
2018-03-21 22:29:58 +01:00
// UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object.
func (c *Container) UpdatePushRules() {
debug.Print("Updating push rules...")
resp, err := c.client.GetPushRules()
if err != nil {
debug.Print("Failed to fetch push rules:", err)
2019-04-27 14:02:21 +02:00
c.config.PushRules = &pushrules.PushRuleset{}
} else {
c.config.PushRules = resp
}
c.config.SavePushRules()
}
2018-03-21 22:29:58 +01:00
// PushRules returns the push notification rules. If no push rules are cached, UpdatePushRules() will be called first.
func (c *Container) PushRules() *pushrules.PushRuleset {
if c.config.PushRules == nil {
c.UpdatePushRules()
}
return c.config.PushRules
}
var AccountDataGomuksPreferences = event.Type{
Type: "net.maunium.gomuks.preferences",
Class: event.AccountDataEventType,
}
2020-04-19 14:00:49 +02:00
func init() {
event.TypeMap[AccountDataGomuksPreferences] = reflect.TypeOf(config.UserPreferences{})
gob.Register(&config.UserPreferences{})
}
2020-04-19 17:06:45 +02:00
type StubSyncingModal struct{}
func (s StubSyncingModal) SetIndeterminate() {}
2020-04-19 17:06:45 +02:00
func (s StubSyncingModal) SetMessage(s2 string) {}
func (s StubSyncingModal) SetSteps(i int) {}
func (s StubSyncingModal) Step() {}
func (s StubSyncingModal) Close() {}
2020-04-19 17:06:45 +02:00
2018-03-21 22:29:58 +01:00
// OnLogin initializes the syncer and updates the room list.
2018-03-18 20:24:03 +01:00
func (c *Container) OnLogin() {
2018-05-01 18:17:57 +02:00
c.ui.OnLogin()
c.client.Store = c.config
2018-03-13 20:58:43 +01:00
2018-04-24 15:51:40 +02:00
debug.Print("Initializing syncer")
2020-04-19 17:06:45 +02:00
c.syncer = NewGomuksSyncer(c.config.Rooms)
2020-05-05 19:38:58 +02:00
if c.crypto != nil {
c.syncer.OnSync(c.crypto.ProcessSyncResponse)
c.syncer.OnEventType(event.StateMember, func(source EventSource, evt *event.Event) {
c.crypto.HandleMemberEvent(evt)
})
c.syncer.OnEventType(event.EventEncrypted, c.HandleEncrypted)
} else {
c.syncer.OnEventType(event.EventEncrypted, c.HandleMessage)
}
c.syncer.OnEventType(event.EventMessage, c.HandleMessage)
c.syncer.OnEventType(event.EventSticker, c.HandleMessage)
c.syncer.OnEventType(event.EventReaction, c.HandleMessage)
c.syncer.OnEventType(event.EventRedaction, c.HandleRedaction)
c.syncer.OnEventType(event.StateAliases, c.HandleMessage)
c.syncer.OnEventType(event.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(event.StateTopic, c.HandleMessage)
c.syncer.OnEventType(event.StateRoomName, c.HandleMessage)
c.syncer.OnEventType(event.StateMember, c.HandleMembership)
c.syncer.OnEventType(event.EphemeralEventReceipt, c.HandleReadReceipt)
c.syncer.OnEventType(event.EphemeralEventTyping, c.HandleTyping)
c.syncer.OnEventType(event.AccountDataDirectChats, c.HandleDirectChatInfo)
c.syncer.OnEventType(event.AccountDataPushRules, c.HandlePushRules)
c.syncer.OnEventType(event.AccountDataRoomTags, c.HandleTag)
c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences)
if len(c.config.AuthCache.NextBatch) == 0 {
c.syncer.Progress = c.ui.MainView().OpenSyncingModal()
c.syncer.Progress.SetMessage("Waiting for /sync response from server")
c.syncer.Progress.SetIndeterminate()
c.syncer.FirstDoneCallback = func() {
c.syncer.Progress.Close()
c.syncer.Progress = StubSyncingModal{}
c.syncer.FirstDoneCallback = nil
}
2020-04-19 17:06:45 +02:00
}
2018-04-24 16:12:08 +02:00
c.syncer.InitDoneCallback = func() {
2018-11-13 23:00:35 +01:00
debug.Print("Initial sync done")
c.config.AuthCache.InitialSyncDone = true
2019-06-15 16:04:08 +02:00
debug.Print("Updating title caches")
for _, room := range c.config.Rooms.Map {
room.GetTitle()
}
debug.Print("Cleaning cached rooms from memory")
c.config.Rooms.ForceClean()
debug.Print("Saving all data")
c.config.SaveAll()
debug.Print("Adding rooms to UI")
2019-06-15 00:11:51 +02:00
c.ui.MainView().SetRooms(c.config.Rooms)
2018-04-24 16:12:08 +02:00
c.ui.Render()
2019-06-16 13:29:03 +02:00
// The initial sync can be a bit heavy, so we force run the GC here
// after cleaning up rooms from memory above.
debug.Print("Running GC")
runtime.GC()
dbg.FreeOSMemory()
2018-04-24 16:12:08 +02:00
}
c.client.Syncer = c.syncer
2018-04-24 15:51:40 +02:00
debug.Print("Setting existing rooms")
c.ui.MainView().SetRooms(c.config.Rooms)
2018-04-24 15:51:40 +02:00
debug.Print("OnLogin() done.")
}
2018-03-21 22:29:58 +01:00
// Start moves the UI to the main view, calls OnLogin() and runs the syncer forever until stopped with Stop()
2018-03-18 20:24:03 +01:00
func (c *Container) Start() {
defer debug.Recover()
c.OnLogin()
2018-03-20 11:16:32 +01:00
if c.client == nil {
return
}
2018-03-18 20:24:03 +01:00
debug.Print("Starting sync...")
c.running = true
for {
select {
case <-c.stop:
2018-03-18 20:24:03 +01:00
debug.Print("Stopping sync...")
c.running = false
return
default:
2018-03-13 20:58:43 +01:00
if err := c.client.Sync(); err != nil {
2018-11-13 23:00:35 +01:00
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.Code == http.StatusUnauthorized {
2018-05-10 14:47:24 +02:00
debug.Print("Sync() errored with ", err, " -> logging out")
c.Logout()
} else {
debug.Print("Sync() errored", err)
}
2018-03-13 20:58:43 +01:00
} else {
2018-03-18 20:24:03 +01:00
debug.Print("Sync() returned without error")
}
}
}
}
func (c *Container) HandlePreferences(source EventSource, evt *event.Event) {
if source&EventSourceAccountData == 0 {
return
}
orig := c.config.Preferences
err := json.Unmarshal(evt.Content.VeryRaw, &c.config.Preferences)
if err != nil {
debug.Print("Failed to parse updated preferences:", err)
return
}
debug.Print("Updated preferences:", orig, "->", c.config.Preferences)
2019-06-15 00:11:51 +02:00
if c.config.AuthCache.InitialSyncDone {
c.ui.HandleNewPreferences()
}
}
func (c *Container) Preferences() *config.UserPreferences {
return &c.config.Preferences
}
func (c *Container) SendPreferencesToMatrix() {
defer debug.Recover()
debug.Print("Sending updated preferences:", c.config.Preferences)
u := c.client.BuildURL("user", string(c.config.UserID), "account_data", AccountDataGomuksPreferences.Type)
_, err := c.client.MakeRequest("PUT", u, &c.config.Preferences, nil)
if err != nil {
debug.Print("Failed to update preferences:", err)
}
}
func (c *Container) HandleRedaction(source EventSource, evt *event.Event) {
2019-06-16 19:42:13 +02:00
room := c.GetOrCreateRoom(evt.RoomID)
var redactedEvt *muksevt.Event
err := c.history.Update(room, evt.Redacts, func(redacted *muksevt.Event) error {
2019-06-16 19:42:13 +02:00
redacted.Unsigned.RedactedBecause = evt
redactedEvt = redacted
return nil
})
if err != nil {
debug.Print("Failed to mark", evt.Redacts, "as redacted:", err)
return
} else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
2019-06-16 19:42:13 +02:00
return
}
roomView := c.ui.MainView().GetRoom(evt.RoomID)
2019-06-16 19:42:13 +02:00
if roomView == nil {
debug.Printf("Failed to handle event %v: No room view found.", evt)
return
}
roomView.AddRedaction(redactedEvt)
if c.syncer.FirstSyncDone {
c.ui.Render()
}
}
func (c *Container) HandleEdit(room *rooms.Room, editsID id.EventID, editEvent *muksevt.Event) {
var origEvt *muksevt.Event
err := c.history.Update(room, editsID, func(evt *muksevt.Event) error {
evt.Gomuks.Edits = append(evt.Gomuks.Edits, editEvent)
origEvt = evt
return nil
})
if err != nil {
debug.Print("Failed to store edit in history db:", err)
return
} else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
return
}
roomView := c.ui.MainView().GetRoom(editEvent.RoomID)
if roomView == nil {
debug.Printf("Failed to handle edit event %v: No room view found.", editEvent)
return
}
roomView.AddEdit(origEvt)
if c.syncer.FirstSyncDone {
c.ui.Render()
2019-06-16 19:42:13 +02:00
}
}
func (c *Container) HandleReaction(room *rooms.Room, reactsTo id.EventID, reactEvent *muksevt.Event) {
2020-04-19 14:00:49 +02:00
rel := reactEvent.Content.AsReaction().RelatesTo
var origEvt *muksevt.Event
err := c.history.Update(room, reactsTo, func(evt *muksevt.Event) error {
2020-02-20 20:56:03 +01:00
if evt.Unsigned.Relations.Annotations.Map == nil {
evt.Unsigned.Relations.Annotations.Map = make(map[string]int)
}
val, _ := evt.Unsigned.Relations.Annotations.Map[rel.Key]
evt.Unsigned.Relations.Annotations.Map[rel.Key] = val + 1
origEvt = evt
return nil
})
if err != nil {
debug.Print("Failed to store reaction in history db:", err)
return
} else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
return
}
roomView := c.ui.MainView().GetRoom(reactEvent.RoomID)
if roomView == nil {
debug.Printf("Failed to handle edit event %v: No room view found.", reactEvent)
return
}
roomView.AddReaction(origEvt, rel.Key)
if c.syncer.FirstSyncDone {
c.ui.Render()
}
}
2020-04-26 23:38:04 +02:00
func (c *Container) HandleEncrypted(source EventSource, mxEvent *event.Event) {
evt, err := c.crypto.DecryptMegolmEvent(mxEvent)
if err != nil {
debug.Print("Failed to decrypt event:", err)
2020-05-05 19:38:58 +02:00
// TODO add decryption failed message instead of passing through directly
c.HandleMessage(source, mxEvent)
2020-04-26 23:38:04 +02:00
return
}
c.HandleMessage(source, evt)
}
2018-03-21 22:29:58 +01:00
// HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, mxEvent *event.Event) {
room := c.GetOrCreateRoom(mxEvent.RoomID)
2019-06-15 16:04:08 +02:00
if source&EventSourceLeave != 0 {
room.HasLeft = true
return
} else if source&EventSourceState != 0 {
return
}
2019-06-15 00:11:51 +02:00
2020-05-05 18:37:35 +02:00
relatable, ok := mxEvent.Content.Parsed.(event.Relatable)
2020-05-05 18:16:25 +02:00
if ok {
rel := relatable.GetRelatesTo()
if editID := rel.GetReplaceID(); len(editID) > 0 {
c.HandleEdit(room, editID, muksevt.Wrap(mxEvent))
return
} else if reactionID := rel.GetAnnotationID(); mxEvent.Type == event.EventReaction && len(reactionID) > 0 {
c.HandleReaction(room, reactionID, muksevt.Wrap(mxEvent))
return
}
}
events, err := c.history.Append(room, []*event.Event{mxEvent})
2019-06-15 00:11:51 +02:00
if err != nil {
debug.Printf("Failed to add event %s to history: %v", mxEvent.ID, err)
2019-06-15 00:11:51 +02:00
}
evt := events[0]
2019-06-15 00:11:51 +02:00
if !c.config.AuthCache.InitialSyncDone {
2019-06-15 16:04:08 +02:00
room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000)
2019-06-15 00:11:51 +02:00
return
}
mainView := c.ui.MainView()
2018-04-24 15:51:40 +02:00
roomView := mainView.GetRoom(evt.RoomID)
if roomView == nil {
2018-04-24 15:51:40 +02:00
debug.Printf("Failed to handle event %v: No room view found.", evt)
return
}
if !room.Loaded() {
pushRules := c.PushRules().GetActions(room, evt.Event).Should()
shouldNotify := pushRules.Notify || !pushRules.NotifySpecified
if !shouldNotify {
room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000)
room.AddUnread(evt.ID, shouldNotify, pushRules.Highlight)
mainView.Bump(room)
return
}
}
message := roomView.AddEvent(evt)
if message != nil {
2019-06-15 00:11:51 +02:00
roomView.MxRoom().LastReceivedMessage = message.Time()
if c.syncer.FirstSyncDone && evt.Sender != c.config.UserID {
pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt.Event).Should()
mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
2018-04-24 15:51:40 +02:00
c.ui.Render()
}
2018-04-24 15:51:40 +02:00
} else {
2020-04-19 14:00:49 +02:00
debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type.Repr(), evt.Content.Raw, evt.Sender, evt.RoomID)
}
2018-03-13 20:58:43 +01:00
}
2018-05-22 16:23:54 +02:00
// HandleMembership is the event handler for the m.room.member state event.
func (c *Container) HandleMembership(source EventSource, evt *event.Event) {
2018-05-22 16:23:54 +02:00
isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0
2019-06-15 16:04:08 +02:00
if isLeave {
c.GetOrCreateRoom(evt.RoomID).HasLeft = true
}
2018-05-22 16:23:54 +02:00
isNonTimelineLeave := isLeave && !isTimeline
if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave {
return
} else if evt.StateKey != nil && id.UserID(*evt.StateKey) == c.config.UserID {
2018-05-22 16:23:54 +02:00
c.processOwnMembershipChange(evt)
} else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) {
// We don't care about other users' membership events in the initial sync or chats we've left.
return
}
c.HandleMessage(source, evt)
}
func (c *Container) processOwnMembershipChange(evt *event.Event) {
2020-04-19 14:00:49 +02:00
membership := evt.Content.AsMember().Membership
prevMembership := event.MembershipLeave
2018-05-22 16:23:54 +02:00
if evt.Unsigned.PrevContent != nil {
2020-04-19 14:00:49 +02:00
prevMembership = evt.Unsigned.PrevContent.AsMember().Membership
2018-05-22 16:23:54 +02:00
}
debug.Printf("Processing own membership change: %s->%s in %s", prevMembership, membership, evt.RoomID)
if membership == prevMembership {
return
}
room := c.GetRoom(evt.RoomID)
switch membership {
case "join":
2020-02-22 00:30:43 +01:00
room.HasLeft = false
if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().UpdateTags(room)
}
2020-02-22 00:30:43 +01:00
fallthrough
case "invite":
2019-06-15 00:11:51 +02:00
if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().AddRoom(room)
}
2018-05-22 16:23:54 +02:00
case "leave":
case "ban":
2019-06-15 00:11:51 +02:00
if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().RemoveRoom(room)
}
2018-05-22 16:23:54 +02:00
room.HasLeft = true
2019-06-15 00:11:51 +02:00
room.Unload()
2020-02-22 00:30:43 +01:00
default:
return
2018-05-22 16:23:54 +02:00
}
2020-02-22 00:30:43 +01:00
c.ui.Render()
2018-05-22 16:23:54 +02:00
}
func (c *Container) parseReadReceipt(evt *event.Event) (largestTimestampEvent id.EventID) {
var largestTimestamp int64
for eventID, rawContent := range evt.Content.Raw {
content, ok := rawContent.(map[string]interface{})
if !ok {
continue
}
mRead, ok := content["m.read"].(map[string]interface{})
if !ok {
continue
}
myInfo, ok := mRead[string(c.config.UserID)].(map[string]interface{})
if !ok {
continue
}
ts, ok := myInfo["ts"].(float64)
if int64(ts) > largestTimestamp {
largestTimestamp = int64(ts)
largestTimestampEvent = id.EventID(eventID)
}
}
return
}
func (c *Container) HandleReadReceipt(source EventSource, evt *event.Event) {
if source&EventSourceLeave != 0 {
return
}
lastReadEvent := c.parseReadReceipt(evt)
if len(lastReadEvent) == 0 {
return
}
room := c.GetRoom(evt.RoomID)
2019-06-15 00:11:51 +02:00
if room != nil {
room.MarkRead(lastReadEvent)
if c.config.AuthCache.InitialSyncDone {
c.ui.Render()
}
}
}
func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]bool {
directChats := make(map[*rooms.Room]bool)
for _, rawRoomIDList := range evt.Content.Raw {
roomIDList, ok := rawRoomIDList.([]interface{})
if !ok {
continue
}
for _, rawRoomID := range roomIDList {
roomID, ok := rawRoomID.(string)
if !ok {
continue
}
room := c.GetOrCreateRoom(id.RoomID(roomID))
if room != nil && !room.HasLeft {
directChats[room] = true
}
}
}
return directChats
}
func (c *Container) HandleDirectChatInfo(_ EventSource, evt *event.Event) {
directChats := c.parseDirectChatInfo(evt)
2019-06-15 00:11:51 +02:00
for _, room := range c.config.Rooms.Map {
shouldBeDirect := directChats[room]
if shouldBeDirect != room.IsDirect {
room.IsDirect = shouldBeDirect
2019-06-15 00:11:51 +02:00
if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().UpdateTags(room)
}
}
}
}
2018-03-21 22:29:58 +01:00
// HandlePushRules is the event handler for the m.push_rules account data event.
func (c *Container) HandlePushRules(_ EventSource, evt *event.Event) {
debug.Print("Received updated push rules")
var err error
c.config.PushRules, err = pushrules.EventToPushRules(evt)
if err != nil {
debug.Print("Failed to convert event to push rules:", err)
return
}
c.config.SavePushRules()
}
2018-03-25 19:30:34 +02:00
// HandleTag is the event handler for the m.tag account data event.
func (c *Container) HandleTag(_ EventSource, evt *event.Event) {
2019-06-15 16:04:08 +02:00
room := c.GetOrCreateRoom(evt.RoomID)
2018-04-24 01:13:17 +02:00
2020-04-19 14:00:49 +02:00
tags := evt.Content.AsTag().Tags
newTags := make([]rooms.RoomTag, len(tags))
2018-04-24 01:13:17 +02:00
index := 0
2020-04-19 14:00:49 +02:00
for tag, info := range tags {
order := json.Number("0.5")
if len(info.Order) > 0 {
order = info.Order
}
2018-04-24 01:13:17 +02:00
newTags[index] = rooms.RoomTag{
Tag: tag,
Order: order,
}
index++
}
room.RawTags = newTags
2019-06-15 00:11:51 +02:00
if c.config.AuthCache.InitialSyncDone {
mainView := c.ui.MainView()
mainView.UpdateTags(room)
}
2018-03-25 19:30:34 +02:00
}
2018-03-21 22:29:58 +01:00
// HandleTyping is the event handler for the m.typing event.
func (c *Container) HandleTyping(_ EventSource, evt *event.Event) {
2019-06-15 00:11:51 +02:00
if !c.config.AuthCache.InitialSyncDone {
return
}
2020-04-19 14:00:49 +02:00
c.ui.MainView().SetTyping(evt.RoomID, evt.Content.AsTyping().UserIDs)
2018-03-14 23:14:39 +01:00
}
func (c *Container) MarkRead(roomID id.RoomID, eventID id.EventID) {
urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID)
_, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil)
}
func (c *Container) PrepareMarkdownMessage(roomID id.RoomID, msgtype event.MessageType, text, html string, rel *ifc.Relation) *muksevt.Event {
2020-04-19 14:00:49 +02:00
var content event.MessageEventContent
if html != "" {
2020-04-19 14:00:49 +02:00
content = event.MessageEventContent{
FormattedBody: html,
Format: event.FormatHTML,
Body: text,
MsgType: msgtype,
}
} else {
content = format.RenderMarkdown(text, !c.config.Preferences.DisableMarkdown, !c.config.Preferences.DisableHTML)
content.MsgType = msgtype
}
if rel != nil && rel.Type == event.RelReplace {
2020-02-19 00:14:02 +01:00
contentCopy := content
content.NewContent = &contentCopy
content.Body = "* " + content.Body
if len(content.FormattedBody) > 0 {
content.FormattedBody = "* " + content.FormattedBody
}
content.RelatesTo = &event.RelatesTo{
Type: event.RelReplace,
EventID: rel.Event.ID,
2020-02-19 00:14:02 +01:00
}
} else if rel != nil && rel.Type == event.RelReference {
content.SetReply(rel.Event.Event)
2020-02-19 00:14:02 +01:00
}
txnID := c.client.TxnID()
localEcho := muksevt.Wrap(&event.Event{
ID: id.EventID(txnID),
Sender: c.config.UserID,
Type: event.EventMessage,
Timestamp: time.Now().UnixNano() / 1e6,
RoomID: roomID,
2020-04-19 14:00:49 +02:00
Content: event.Content{Parsed: &content},
Unsigned: event.Unsigned{TransactionID: txnID},
})
localEcho.Gomuks.OutgoingState = muksevt.StateLocalEcho
if rel != nil && rel.Type == event.RelReplace {
localEcho.ID = rel.Event.ID
localEcho.Gomuks.Edits = []*muksevt.Event{localEcho}
2020-02-19 00:14:02 +01:00
}
2019-04-10 00:04:39 +02:00
return localEcho
}
func (c *Container) Redact(roomID id.RoomID, eventID id.EventID, reason string) error {
2020-03-01 21:35:21 +01:00
defer debug.Recover()
_, err := c.client.RedactEvent(roomID, eventID, mautrix.ReqRedact{Reason: reason})
return err
}
// SendMessage sends the given event.
2020-04-19 14:00:49 +02:00
func (c *Container) SendEvent(evt *muksevt.Event) (id.EventID, error) {
2019-04-10 00:04:39 +02:00
defer debug.Recover()
_, _ = c.client.UserTyping(evt.RoomID, false, 0)
c.typing = 0
room := c.GetRoom(evt.RoomID)
2020-05-05 19:38:58 +02:00
if room != nil && room.Encrypted && c.crypto != nil && evt.Type != event.EventReaction {
encrypted, err := c.crypto.EncryptMegolmEvent(evt.RoomID, evt.Type, evt.Content)
if err != nil {
2020-05-05 19:38:58 +02:00
if isBadEncryptError(err) {
return "", err
}
debug.Print("Got", err, "while trying to encrypt message, sharing group session and trying again...")
err = c.crypto.ShareGroupSession(room.ID, room.GetMemberList())
if err != nil {
return "", err
}
encrypted, err = c.crypto.EncryptMegolmEvent(evt.RoomID, evt.Type, evt.Content)
if err != nil {
return "", err
}
}
evt.Type = event.EventEncrypted
evt.Content = event.Content{Parsed: encrypted}
}
2020-04-19 14:00:49 +02:00
resp, err := c.client.SendMessageEvent(evt.RoomID, evt.Type, &evt.Content, mautrix.ReqSendEvent{TransactionID: evt.Unsigned.TransactionID})
if err != nil {
return "", err
}
return resp.EventID, nil
}
func (c *Container) sendTypingAsync(roomID id.RoomID, typing bool, timeout int64) {
defer debug.Recover()
_, _ = c.client.UserTyping(roomID, typing, timeout)
}
2018-03-21 22:29:58 +01:00
// SendTyping sets whether or not the user is typing in the given room.
func (c *Container) SendTyping(roomID id.RoomID, typing bool) {
2018-03-18 20:24:03 +01:00
ts := time.Now().Unix()
if (c.typing > ts && typing) || (c.typing == 0 && !typing) {
2018-03-14 23:14:39 +01:00
return
}
2018-03-15 18:45:52 +01:00
if typing {
go c.sendTypingAsync(roomID, true, 20000)
2018-03-21 22:29:58 +01:00
c.typing = ts + 15
2018-03-15 18:45:52 +01:00
} else {
go c.sendTypingAsync(roomID, false, 0)
2018-03-15 18:45:52 +01:00
c.typing = 0
}
2018-03-14 23:14:39 +01:00
}
// CreateRoom attempts to create a new room and join the user.
func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) {
resp, err := c.client.CreateRoom(req)
if err != nil {
return nil, err
}
2019-06-15 00:11:51 +02:00
room := c.GetOrCreateRoom(resp.RoomID)
return room, nil
}
2018-03-21 22:29:58 +01:00
// JoinRoom makes the current user try to join the given room.
func (c *Container) JoinRoom(roomID id.RoomID, server string) (*rooms.Room, error) {
resp, err := c.client.JoinRoom(string(roomID), server, nil)
2018-03-18 20:24:03 +01:00
if err != nil {
return nil, err
2018-03-18 20:24:03 +01:00
}
2020-02-18 20:41:49 +01:00
room := c.GetOrCreateRoom(resp.RoomID)
room.HasLeft = false
return room, nil
2018-03-18 20:24:03 +01:00
}
2018-03-21 22:29:58 +01:00
// LeaveRoom makes the current user leave the given room.
func (c *Container) LeaveRoom(roomID id.RoomID) error {
2018-03-18 20:24:03 +01:00
_, err := c.client.LeaveRoom(roomID)
2018-03-16 15:24:11 +01:00
if err != nil {
return err
}
2019-06-15 00:11:51 +02:00
node := c.GetOrCreateRoom(roomID)
node.HasLeft = true
node.Unload()
2018-03-16 15:24:11 +01:00
return nil
}
2020-02-21 23:03:57 +01:00
func (c *Container) FetchMembers(room *rooms.Room) error {
2020-04-20 21:46:41 +02:00
debug.Print("Fetching member list for", room.ID)
2020-02-21 23:03:57 +01:00
members, err := c.client.Members(room.ID, mautrix.ReqMembers{At: room.LastPrevBatch})
if err != nil {
return err
}
2020-04-20 21:46:41 +02:00
debug.Printf("Fetched %d members for %s", len(members.Chunk), room.ID)
2020-02-21 23:03:57 +01:00
for _, evt := range members.Chunk {
2020-04-20 21:46:41 +02:00
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
debug.Printf("Failed to parse member event of %s: %v", evt.GetStateKey(), err)
continue
}
2020-02-21 23:03:57 +01:00
room.UpdateState(evt)
}
room.MembersFetched = true
return nil
}
2018-03-21 22:29:58 +01:00
// GetHistory fetches room history.
func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*muksevt.Event, error) {
events, err := c.history.Load(room, limit)
if err != nil {
return nil, err
}
if len(events) > 0 {
debug.Printf("Loaded %d events for %s from local cache", len(events), room.ID)
return events, nil
}
resp, err := c.client.Messages(room.ID, room.PrevBatch, "", 'b', limit)
if err != nil {
return nil, err
}
debug.Printf("Loaded %d events for %s from server from %s to %s", len(resp.Chunk), room.ID, resp.Start, resp.End)
for i, evt := range resp.Chunk {
2020-04-19 14:00:49 +02:00
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
debug.Printf("Failed to unmarshal content of event %s (type %s) by %s in %s: %v\n%s", evt.ID, evt.Type.Repr(), evt.Sender, evt.RoomID, err, string(evt.Content.VeryRaw))
}
2020-05-05 19:38:58 +02:00
if c.crypto != nil && evt.Type == event.EventEncrypted {
decrypted, err := c.crypto.DecryptMegolmEvent(evt)
if err != nil {
debug.Print("Failed to decrypt event:", err)
2020-05-05 19:38:58 +02:00
// TODO add decryption failed message instead of passing through directly
} else {
resp.Chunk[i] = decrypted
}
}
2020-04-19 14:00:49 +02:00
}
2020-02-21 23:03:57 +01:00
for _, evt := range resp.State {
room.UpdateState(evt)
}
2019-06-15 00:11:51 +02:00
room.PrevBatch = resp.End
c.config.Rooms.Put(room)
if len(resp.Chunk) == 0 {
return []*muksevt.Event{}, nil
}
events, err = c.history.Prepend(room, resp.Chunk)
if err != nil {
return nil, err
}
return events, nil
}
func (c *Container) GetEvent(room *rooms.Room, eventID id.EventID) (*muksevt.Event, error) {
evt, err := c.history.Get(room, eventID)
if err != nil && err != EventNotFoundError {
debug.Printf("Failed to get event %s from local cache: %v", eventID, err)
} else if evt != nil {
debug.Printf("Found event %s in local cache", eventID)
return evt, err
}
mxEvent, err := c.client.GetEvent(room.ID, eventID)
if err != nil {
return nil, err
}
evt = muksevt.Wrap(mxEvent)
debug.Printf("Loaded event %s from server", eventID)
return evt, nil
}
2019-06-15 00:11:51 +02:00
// GetOrCreateRoom gets the room instance stored in the session.
func (c *Container) GetOrCreateRoom(roomID id.RoomID) *rooms.Room {
2019-06-15 00:11:51 +02:00
return c.config.Rooms.GetOrCreate(roomID)
}
2018-03-21 22:29:58 +01:00
// GetRoom gets the room instance stored in the session.
func (c *Container) GetRoom(roomID id.RoomID) *rooms.Room {
2019-06-15 00:11:51 +02:00
return c.config.Rooms.Get(roomID)
2018-03-14 23:14:39 +01:00
}
func cp(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}
2020-04-29 01:45:54 +02:00
func (c *Container) DownloadToDisk(uri id.ContentURI, file *attachment.EncryptedFile, target string) (fullPath string, err error) {
cachePath := c.GetCachePath(uri)
if target == "" {
fullPath = cachePath
} else if !path.IsAbs(target) {
fullPath = path.Join(c.config.DownloadDir, target)
} else {
fullPath = target
}
if _, statErr := os.Stat(cachePath); os.IsNotExist(statErr) {
2020-04-29 01:45:54 +02:00
var body io.ReadCloser
body, err = c.client.Download(uri)
if err != nil {
return
}
2020-04-29 01:45:54 +02:00
var data []byte
data, err = ioutil.ReadAll(body)
_ = body.Close()
if err != nil {
return
}
2020-04-29 01:45:54 +02:00
if file != nil {
data, err = file.Decrypt(data)
if err != nil {
return
}
}
err = ioutil.WriteFile(cachePath, data, 0600)
if err != nil {
return
}
}
2018-04-15 13:03:05 +02:00
if fullPath != cachePath {
err = os.MkdirAll(path.Dir(fullPath), 0700)
if err != nil {
return
}
err = cp(cachePath, fullPath)
}
return
}
// Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors.
//
// The file will be either read from the media cache (if found) or downloaded from the server.
2020-04-29 01:45:54 +02:00
func (c *Container) Download(uri id.ContentURI, file *attachment.EncryptedFile) (data []byte, err error) {
cacheFile := c.GetCachePath(uri)
var info os.FileInfo
if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() {
data, err = ioutil.ReadFile(cacheFile)
if err == nil {
return
}
}
2020-04-29 01:45:54 +02:00
data, err = c.download(uri, file, cacheFile)
2018-04-15 13:03:05 +02:00
return
}
func (c *Container) GetDownloadURL(uri id.ContentURI) string {
return c.client.GetDownloadURL(uri)
}
2020-04-29 01:45:54 +02:00
func (c *Container) download(uri id.ContentURI, file *attachment.EncryptedFile, cacheFile string) (data []byte, err error) {
var body io.ReadCloser
body, err = c.client.Download(uri)
if err != nil {
return
}
2020-04-29 01:45:54 +02:00
data, err = ioutil.ReadAll(body)
_ = body.Close()
if err != nil {
return
}
2020-04-29 01:45:54 +02:00
if file != nil {
data, err = file.Decrypt(data)
if err != nil {
return
}
}
err = ioutil.WriteFile(cacheFile, data, 0600)
return
}
2018-05-01 18:17:57 +02:00
// GetCachePath gets the path to the cached version of the given homeserver:fileID combination.
// The file may or may not exist, use Download() to ensure it has been cached.
func (c *Container) GetCachePath(uri id.ContentURI) string {
dir := filepath.Join(c.config.MediaDir, uri.Homeserver)
err := os.MkdirAll(dir, 0700)
if err != nil {
return ""
}
return filepath.Join(dir, uri.FileID)
}