2018-03-13 14:27:12 +01:00
// gomuks - A terminal Matrix client written in Go.
2019-01-17 13:13:25 +01:00
// Copyright (C) 2019 Tulir Asokan
2018-03-13 14:27:12 +01:00
//
// 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
2018-03-13 14:27:12 +01:00
// 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.
2018-03-13 14:27:12 +01:00
//
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-13 14:27:12 +01:00
2018-03-18 20:24:03 +01:00
package matrix
2018-03-13 14:27:12 +01:00
import (
2018-04-10 18:31:28 +02:00
"bytes"
2020-02-18 20:31:28 +01:00
"context"
2019-01-17 13:13:25 +01:00
"crypto/tls"
"encoding/json"
2018-03-13 14:27:12 +01:00
"fmt"
2018-04-10 18:31:28 +02:00
"io"
"io/ioutil"
2018-04-11 16:57:15 +02:00
"net/http"
2018-04-10 18:31:28 +02:00
"net/url"
"os"
"path"
"path/filepath"
"regexp"
2019-06-16 13:29:03 +02:00
"runtime"
dbg "runtime/debug"
2019-06-19 21:27:48 +02:00
"time"
2018-03-13 14:27:12 +01:00
2020-02-18 20:31:28 +01:00
"maunium.net/go/gomuks/lib/open"
2019-06-17 11:27:31 +02:00
"maunium.net/go/gomuks/matrix/event"
2019-01-17 13:13:25 +01:00
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/format"
2018-04-10 18:31:28 +02:00
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
2018-03-13 14:27:12 +01:00
)
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
2018-04-01 08:53:00 +02:00
syncer * GomuksSyncer
2018-03-18 20:24:03 +01:00
gmx ifc . Gomuks
ui ifc . GomuksUI
config * config . Config
2019-04-05 22:44:17 +02:00
history * HistoryManager
2018-03-13 14:27:12 +01:00
running bool
stop chan bool
2018-03-14 23:14:39 +01:00
typing int64
2018-03-13 14:27:12 +01:00
}
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 14:27:12 +01:00
}
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 {
2018-05-01 20:04:25 +02:00
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 ... )
}
// 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 {
2018-03-13 14:27:12 +01:00
if len ( c . config . HS ) == 0 {
return fmt . Errorf ( "no homeserver in config" )
}
2018-03-13 20:58:43 +01:00
if c . client != nil {
c . Stop ( )
c . client = nil
}
2018-03-13 14:27:12 +01:00
var mxid , accessToken string
2018-05-17 15:29:15 +02:00
if len ( c . config . AccessToken ) > 0 {
accessToken = c . config . AccessToken
2018-03-21 22:29:58 +01:00
mxid = c . config . UserID
2018-03-13 14:27:12 +01:00
}
var err error
2018-11-13 23:00:35 +01:00
c . client , err = mautrix . NewClient ( c . config . HS , mxid , accessToken )
2018-03-13 14:27:12 +01:00
if err != nil {
return err
}
2018-11-13 23:00:35 +01:00
c . client . Logger = mxLogger { }
2018-03-13 14:27:12 +01:00
2019-04-05 22:44:17 +02:00
c . history , err = NewHistoryManager ( c . config . HistoryPath )
if err != nil {
return err
}
2018-05-23 00:16:05 +02:00
allowInsecure := len ( os . Getenv ( "GOMUKS_ALLOW_INSECURE_CONNECTIONS" ) ) > 0
2018-05-23 00:05:56 +02:00
if allowInsecure {
c . client . Client = & http . Client {
Transport : & http . Transport { TLSClientConfig : & tls . Config { InsecureSkipVerify : true } } ,
}
}
2018-03-13 14:27:12 +01:00
c . stop = make ( chan bool , 1 )
2018-05-17 15:29:15 +02:00
if len ( accessToken ) > 0 {
2018-03-13 14:27:12 +01:00
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
}
2020-02-18 20:31:28 +01:00
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" ,
2018-03-13 14:27:12 +01:00
} )
if err != nil {
return err
}
2020-02-18 20:31:28 +01:00
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 )
2018-03-21 22:29:58 +01:00
c . config . UserID = resp . UserID
2018-05-17 15:29:15 +02:00
c . config . AccessToken = resp . AccessToken
2018-03-13 14:27:12 +01:00
c . config . Save ( )
go c . Start ( )
2020-02-18 20:31:28 +01:00
}
2018-03-13 14:27:12 +01:00
2020-02-18 20:31:28 +01:00
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 ( [ ] string { "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
2020-02-18 20:31:28 +01:00
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-03-13 14:27:12 +01:00
}
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 . config . DeleteSession ( )
c . Stop ( )
c . client = nil
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 ( )
2019-04-05 22:44:17 +02:00
debug . Print ( "Closing history manager..." )
err := c . history . Close ( )
if err != nil {
debug . Print ( "Error closing history manager:" , 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.
2018-03-20 22:34:43 +01:00
func ( c * Container ) UpdatePushRules ( ) {
2018-03-21 19:01:52 +01:00
debug . Print ( "Updating push rules..." )
2018-03-21 22:29:58 +01:00
resp , err := pushrules . GetPushRules ( c . client )
2018-03-20 22:34:43 +01:00
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
2018-03-20 22:34:43 +01:00
}
2018-05-17 15:29:15 +02:00
c . config . SavePushRules ( )
2018-03-20 22:34:43 +01:00
}
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 {
2018-05-17 15:29:15 +02:00
if c . config . PushRules == nil {
2018-03-21 17:46:19 +01:00
c . UpdatePushRules ( )
}
2018-05-17 15:29:15 +02:00
return c . config . PushRules
2018-03-21 17:46:19 +01:00
}
2018-11-13 23:00:35 +01:00
var AccountDataGomuksPreferences = mautrix . NewEventType ( "net.maunium.gomuks.preferences" )
2018-09-05 09:55:48 +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 ( )
2018-05-17 15:29:15 +02:00
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" )
2018-05-17 15:29:15 +02:00
c . syncer = NewGomuksSyncer ( c . config )
2018-11-13 23:00:35 +01:00
c . syncer . OnEventType ( mautrix . EventMessage , c . HandleMessage )
2019-06-15 00:11:51 +02:00
// Just pass encrypted events as messages, they'll show up with an encryption unsupported message.
c . syncer . OnEventType ( mautrix . EventEncrypted , c . HandleMessage )
c . syncer . OnEventType ( mautrix . EventSticker , c . HandleMessage )
2019-06-16 19:42:13 +02:00
c . syncer . OnEventType ( mautrix . EventRedaction , c . HandleRedaction )
2019-01-11 22:28:47 +01:00
c . syncer . OnEventType ( mautrix . StateAliases , c . HandleMessage )
c . syncer . OnEventType ( mautrix . StateCanonicalAlias , c . HandleMessage )
c . syncer . OnEventType ( mautrix . StateTopic , c . HandleMessage )
c . syncer . OnEventType ( mautrix . StateRoomName , c . HandleMessage )
2018-11-13 23:00:35 +01:00
c . syncer . OnEventType ( mautrix . StateMember , c . HandleMembership )
c . syncer . OnEventType ( mautrix . EphemeralEventReceipt , c . HandleReadReceipt )
c . syncer . OnEventType ( mautrix . EphemeralEventTyping , c . HandleTyping )
c . syncer . OnEventType ( mautrix . AccountDataDirectChats , c . HandleDirectChatInfo )
c . syncer . OnEventType ( mautrix . AccountDataPushRules , c . HandlePushRules )
c . syncer . OnEventType ( mautrix . AccountDataRoomTags , c . HandleTag )
2018-09-05 09:55:48 +02:00
c . syncer . OnEventType ( AccountDataGomuksPreferences , c . HandlePreferences )
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" )
2018-05-17 15:29:15 +02:00
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
}
2018-04-01 08:53:00 +02:00
c . client . Syncer = c . syncer
2018-03-13 14:27:12 +01:00
2018-04-24 15:51:40 +02:00
debug . Print ( "Setting existing rooms" )
2018-05-17 15:29:15 +02:00
c . ui . MainView ( ) . SetRooms ( c . config . Rooms )
2018-04-24 15:51:40 +02:00
debug . Print ( "OnLogin() done." )
2018-03-18 16:34:42 +01:00
}
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 ( ) {
2018-04-18 17:35:24 +02:00
defer debug . Recover ( )
2018-03-18 16:34:42 +01:00
c . OnLogin ( )
2018-03-17 14:48:31 +01:00
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..." )
2018-03-17 14:48:31 +01:00
c . running = true
2018-03-13 14:27:12 +01:00
for {
select {
case <- c . stop :
2018-03-18 20:24:03 +01:00
debug . Print ( "Stopping sync..." )
2018-03-13 14:27:12 +01:00
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" )
2018-03-13 14:27:12 +01:00
}
}
}
}
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandlePreferences ( source EventSource , evt * mautrix . Event ) {
2019-04-07 22:21:48 +02:00
if source & EventSourceAccountData == 0 {
return
}
2018-05-24 22:26:57 +02:00
orig := c . config . Preferences
2019-04-07 22:21:48 +02:00
err := json . Unmarshal ( evt . Content . VeryRaw , & c . config . Preferences )
if err != nil {
debug . Print ( "Failed to parse updated preferences:" , err )
return
}
2018-05-24 22:26:57 +02:00
debug . Print ( "Updated preferences:" , orig , "->" , c . config . Preferences )
2019-06-15 00:11:51 +02:00
if c . config . AuthCache . InitialSyncDone {
c . ui . HandleNewPreferences ( )
}
2018-05-24 22:26:57 +02:00
}
func ( c * Container ) SendPreferencesToMatrix ( ) {
defer debug . Recover ( )
debug . Print ( "Sending updated preferences:" , c . config . Preferences )
u := c . client . BuildURL ( "user" , c . config . UserID , "account_data" , "net.maunium.gomuks.preferences" )
_ , err := c . client . MakeRequest ( "PUT" , u , & c . config . Preferences , nil )
if err != nil {
debug . Print ( "Failed to update preferences:" , err )
}
}
2019-06-16 19:42:13 +02:00
func ( c * Container ) HandleRedaction ( source EventSource , evt * mautrix . Event ) {
room := c . GetOrCreateRoom ( evt . RoomID )
2019-06-17 11:27:31 +02:00
var redactedEvt * event . Event
err := c . history . Update ( room , evt . Redacts , func ( redacted * event . Event ) error {
2019-06-16 19:42:13 +02:00
redacted . Unsigned . RedactedBy = evt . ID
redacted . Unsigned . RedactedBecause = evt
redactedEvt = redacted
return nil
} )
if err != nil {
debug . Print ( "Failed to mark" , evt . Redacts , "as redacted:" , err )
2019-06-19 21:27:48 +02:00
return
} else if ! c . config . AuthCache . InitialSyncDone || ! room . Loaded ( ) {
2019-06-16 19:42:13 +02:00
return
}
2019-06-19 21:27:48 +02:00
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
}
2019-06-17 12:46:02 +02:00
roomView . AddRedaction ( redactedEvt )
if c . syncer . FirstSyncDone {
c . ui . Render ( )
}
}
func ( c * Container ) HandleEdit ( room * rooms . Room , editsID string , editEvent * event . Event ) {
var origEvt * event . Event
err := c . history . Update ( room , editsID , func ( evt * event . 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 )
2019-06-19 21:27:48 +02:00
return
} else if ! c . config . AuthCache . InitialSyncDone || ! room . Loaded ( ) {
2019-06-17 12:46:02 +02:00
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
}
}
2018-03-21 22:29:58 +01:00
// HandleMessage is the event handler for the m.room.message timeline event.
2019-06-17 11:27:31 +02:00
func ( c * Container ) HandleMessage ( source EventSource , mxEvent * mautrix . 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 {
2018-04-30 21:28:29 +02:00
return
}
2019-06-15 00:11:51 +02:00
2019-06-17 12:46:02 +02:00
if editID := mxEvent . Content . GetRelatesTo ( ) . GetReplaceID ( ) ; len ( editID ) > 0 {
c . HandleEdit ( room , editID , event . Wrap ( mxEvent ) )
return
}
2019-06-17 11:27:31 +02:00
events , err := c . history . Append ( room , [ ] * mautrix . Event { mxEvent } )
2019-06-15 00:11:51 +02:00
if err != nil {
2019-06-17 11:27:31 +02:00
debug . Printf ( "Failed to add event %s to history: %v" , mxEvent . ID , err )
2019-06-15 00:11:51 +02:00
}
2019-06-17 11:27:31 +02:00
evt := events [ 0 ]
2019-06-15 00:11:51 +02:00
2019-06-15 16:51:36 +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
}
2018-03-26 16:22:47 +02:00
mainView := c . ui . MainView ( )
2018-04-24 15:51:40 +02:00
2018-03-26 16:22:47 +02:00
roomView := mainView . GetRoom ( evt . RoomID )
2018-03-23 00:00:13 +01:00
if roomView == nil {
2018-04-24 15:51:40 +02:00
debug . Printf ( "Failed to handle event %v: No room view found." , evt )
2018-03-23 00:00:13 +01:00
return
}
2019-06-15 16:51:36 +02:00
if ! room . Loaded ( ) {
2019-06-17 11:27:31 +02:00
pushRules := c . PushRules ( ) . GetActions ( room , evt . Event ) . Should ( )
2019-06-15 16:51:36 +02:00
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
}
}
2019-06-17 12:46:02 +02:00
message := roomView . AddEvent ( evt )
2018-03-23 00:00:13 +01:00
if message != nil {
2019-06-15 00:11:51 +02:00
roomView . MxRoom ( ) . LastReceivedMessage = message . Time ( )
2018-04-01 08:53:00 +02:00
if c . syncer . FirstSyncDone {
2019-06-17 11:27:31 +02:00
pushRules := c . PushRules ( ) . GetActions ( roomView . MxRoom ( ) , evt . Event ) . Should ( )
2018-04-09 22:45:54 +02:00
mainView . NotifyMessage ( roomView . MxRoom ( ) , message , pushRules )
2018-04-24 15:51:40 +02:00
c . ui . Render ( )
2018-04-01 08:53:00 +02:00
}
2018-04-24 15:51:40 +02:00
} else {
2019-06-15 16:04:08 +02:00
debug . Printf ( "Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil)." , evt . ID , evt . Type . String ( ) , evt . Content . Raw , evt . Sender , evt . RoomID )
2018-03-17 14:48:31 +01:00
}
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.
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandleMembership ( source EventSource , evt * mautrix . 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 && * evt . StateKey == c . config . UserID {
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 )
}
2018-11-13 23:00:35 +01:00
func ( c * Container ) processOwnMembershipChange ( evt * mautrix . Event ) {
2018-09-05 09:55:48 +02:00
membership := evt . Content . Membership
2018-11-13 23:00:35 +01:00
prevMembership := mautrix . MembershipLeave
2018-05-22 16:23:54 +02:00
if evt . Unsigned . PrevContent != nil {
2018-09-05 09:55:48 +02:00
prevMembership = evt . Unsigned . PrevContent . 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" :
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
room . HasLeft = false
case "leave" :
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 ( )
2018-05-22 16:23:54 +02:00
case "invite" :
// TODO handle
debug . Printf ( "%s invited the user to %s" , evt . Sender , evt . RoomID )
}
}
2018-11-13 23:00:35 +01:00
func ( c * Container ) parseReadReceipt ( evt * mautrix . Event ) ( largestTimestampEvent string ) {
2018-05-16 19:09:09 +02:00
var largestTimestamp int64
2018-09-05 09:55:48 +02:00
for eventID , rawContent := range evt . Content . Raw {
2018-05-16 19:09:09 +02:00
content , ok := rawContent . ( map [ string ] interface { } )
if ! ok {
continue
}
mRead , ok := content [ "m.read" ] . ( map [ string ] interface { } )
if ! ok {
continue
}
2018-05-17 15:29:15 +02:00
myInfo , ok := mRead [ c . config . UserID ] . ( map [ string ] interface { } )
2018-05-16 19:09:09 +02:00
if ! ok {
continue
}
ts , ok := myInfo [ "ts" ] . ( float64 )
if int64 ( ts ) > largestTimestamp {
largestTimestamp = int64 ( ts )
largestTimestampEvent = eventID
}
}
return
}
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandleReadReceipt ( source EventSource , evt * mautrix . Event ) {
2018-05-16 19:09:09 +02:00
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 ( )
}
}
2018-05-16 19:09:09 +02:00
}
2018-11-13 23:00:35 +01:00
func ( c * Container ) parseDirectChatInfo ( evt * mautrix . Event ) map [ * rooms . Room ] bool {
2018-05-16 19:09:09 +02:00
directChats := make ( map [ * rooms . Room ] bool )
2018-09-05 09:55:48 +02:00
for _ , rawRoomIDList := range evt . Content . Raw {
2018-05-16 19:09:09 +02:00
roomIDList , ok := rawRoomIDList . ( [ ] interface { } )
if ! ok {
continue
}
for _ , rawRoomID := range roomIDList {
roomID , ok := rawRoomID . ( string )
if ! ok {
continue
}
2019-06-15 16:04:08 +02:00
room := c . GetOrCreateRoom ( roomID )
2018-05-16 19:09:09 +02:00
if room != nil && ! room . HasLeft {
directChats [ room ] = true
}
}
}
return directChats
}
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandleDirectChatInfo ( source EventSource , evt * mautrix . Event ) {
2018-05-16 19:09:09 +02:00
directChats := c . parseDirectChatInfo ( evt )
2019-06-15 00:11:51 +02:00
for _ , room := range c . config . Rooms . Map {
2018-05-16 19:09:09 +02:00
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-05-16 19:09:09 +02:00
}
}
}
2018-03-21 22:29:58 +01:00
// HandlePushRules is the event handler for the m.push_rules account data event.
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandlePushRules ( source EventSource , evt * mautrix . Event ) {
2018-03-21 19:01:52 +01:00
debug . Print ( "Received updated push rules" )
var err error
2018-05-17 15:29:15 +02:00
c . config . PushRules , err = pushrules . EventToPushRules ( evt )
2018-03-21 19:01:52 +01:00
if err != nil {
debug . Print ( "Failed to convert event to push rules:" , err )
2018-05-17 15:29:15 +02:00
return
2018-03-21 19:01:52 +01:00
}
2018-05-17 15:29:15 +02:00
c . config . SavePushRules ( )
2018-03-21 19:01:52 +01:00
}
2018-03-25 19:30:34 +02:00
// HandleTag is the event handler for the m.tag account data event.
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandleTag ( source EventSource , evt * mautrix . Event ) {
2019-06-15 16:04:08 +02:00
room := c . GetOrCreateRoom ( evt . RoomID )
2018-04-24 01:13:17 +02:00
2018-09-05 09:55:48 +02:00
newTags := make ( [ ] rooms . RoomTag , len ( evt . Content . RoomTags ) )
2018-04-24 01:13:17 +02:00
index := 0
2018-09-05 09:55:48 +02:00
for tag , info := range evt . Content . RoomTags {
2018-05-03 23:55:28 +02:00
order := "0.5"
2018-09-05 09:55:48 +02:00
if len ( info . Order ) > 0 {
2018-11-13 23:00:35 +01:00
order = info . Order . String ( )
2018-05-03 23:55:28 +02:00
}
2018-04-24 01:13:17 +02:00
newTags [ index ] = rooms . RoomTag {
Tag : tag ,
Order : order ,
}
index ++
}
2018-05-16 19:09:09 +02:00
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.
2018-11-13 23:00:35 +01:00
func ( c * Container ) HandleTyping ( source EventSource , evt * mautrix . Event ) {
2019-06-15 00:11:51 +02:00
if ! c . config . AuthCache . InitialSyncDone {
return
}
2018-09-05 09:55:48 +02:00
c . ui . MainView ( ) . SetTyping ( evt . RoomID , evt . Content . TypingUserIDs )
2018-03-14 23:14:39 +01:00
}
2018-05-16 19:09:09 +02:00
func ( c * Container ) MarkRead ( roomID , eventID string ) {
urlPath := c . client . BuildURL ( "rooms" , roomID , "receipt" , "m.read" , eventID )
c . client . MakeRequest ( "POST" , urlPath , struct { } { } , nil )
}
2018-04-21 19:19:43 +02:00
var mentionRegex = regexp . MustCompile ( "\\[(.+?)]\\(https://matrix.to/#/@.+?:.+?\\)" )
var roomRegex = regexp . MustCompile ( "\\[.+?]\\(https://matrix.to/#/(#.+?:[^/]+?)\\)" )
2020-02-19 00:14:02 +01:00
func ( c * Container ) PrepareMarkdownMessage ( roomID string , msgtype mautrix . MessageType , text string , edit * event . Event ) * event . Event {
2019-03-28 22:28:27 +01:00
content := format . RenderMarkdown ( text )
content . MsgType = msgtype
2018-04-18 13:20:57 +02:00
2018-04-21 19:19:43 +02:00
// Remove markdown link stuff from plaintext mentions and room links
2019-03-28 22:28:27 +01:00
content . Body = mentionRegex . ReplaceAllString ( content . Body , "$1" )
content . Body = roomRegex . ReplaceAllString ( content . Body , "$1" )
2018-04-21 19:19:43 +02:00
2020-02-19 00:14:02 +01:00
if edit != nil {
contentCopy := content
content . NewContent = & contentCopy
content . Body = "* " + content . Body
if len ( content . FormattedBody ) > 0 {
content . FormattedBody = "* " + content . FormattedBody
}
content . RelatesTo = & mautrix . RelatesTo {
Type : mautrix . RelReplace ,
EventID : edit . ID ,
}
}
2019-04-09 17:45:41 +02:00
txnID := c . client . TxnID ( )
2019-06-17 11:27:31 +02:00
localEcho := event . Wrap ( & mautrix . Event {
2019-04-10 00:04:39 +02:00
ID : txnID ,
2019-04-09 17:45:41 +02:00
Sender : c . config . UserID ,
Type : mautrix . EventMessage ,
Timestamp : time . Now ( ) . UnixNano ( ) / 1e6 ,
RoomID : roomID ,
Content : content ,
Unsigned : mautrix . Unsigned {
TransactionID : txnID ,
} ,
2019-06-17 11:27:31 +02:00
} )
localEcho . Gomuks . OutgoingState = event . StateLocalEcho
2020-02-19 00:14:02 +01:00
if edit != nil {
localEcho . ID = edit . ID
localEcho . Gomuks . Edits = [ ] * event . Event { localEcho }
}
2019-04-10 00:04:39 +02:00
return localEcho
}
// SendMarkdownMessage sends a message with the given markdown text to the given room.
2019-06-17 11:27:31 +02:00
func ( c * Container ) SendEvent ( event * event . Event ) ( string , error ) {
2019-04-10 00:04:39 +02:00
defer debug . Recover ( )
c . SendTyping ( event . RoomID , false )
resp , err := c . client . SendMessageEvent ( event . RoomID , event . Type , event . Content , mautrix . ReqSendEvent { TransactionID : event . Unsigned . TransactionID } )
2018-04-18 13:20:57 +02:00
if err != nil {
return "" , err
}
return resp . EventID , nil
}
2018-03-21 22:29:58 +01:00
// SendTyping sets whether or not the user is typing in the given room.
2018-03-18 20:24:03 +01:00
func ( c * Container ) SendTyping ( roomID string , typing bool ) {
2018-04-18 17:35:24 +02:00
defer debug . Recover ( )
2018-03-18 20:24:03 +01:00
ts := time . Now ( ) . Unix ( )
if c . typing > ts && typing {
2018-03-14 23:14:39 +01:00
return
}
2018-03-15 18:45:52 +01:00
if typing {
2018-03-21 22:29:58 +01:00
c . client . UserTyping ( roomID , true , 20000 )
c . typing = ts + 15
2018-03-15 18:45:52 +01:00
} else {
c . client . UserTyping ( roomID , false , 0 )
c . typing = 0
}
2018-03-14 23:14:39 +01:00
}
2019-06-14 03:14:32 +02: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 )
2019-06-14 03:14:32 +02:00
return room , nil
}
2018-03-21 22:29:58 +01:00
// JoinRoom makes the current user try to join the given room.
2018-05-10 19:07:24 +02:00
func ( c * Container ) JoinRoom ( roomID , server string ) ( * rooms . Room , error ) {
2018-04-30 22:09:14 +02:00
resp , err := c . client . JoinRoom ( roomID , server , nil )
2018-03-18 20:24:03 +01:00
if err != nil {
2018-04-30 22:09:14 +02:00
return nil , err
2018-03-18 20:24:03 +01:00
}
2020-02-18 20:41:49 +01:00
room := c . GetOrCreateRoom ( resp . RoomID )
2018-04-30 22:09:14 +02:00
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.
2018-03-18 20:24:03 +01:00
func ( c * Container ) LeaveRoom ( roomID string ) error {
_ , 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
}
2018-03-21 22:29:58 +01:00
// GetHistory fetches room history.
2019-06-17 11:27:31 +02:00
func ( c * Container ) GetHistory ( room * rooms . Room , limit int ) ( [ ] * event . Event , error ) {
2019-04-05 22:44:17 +02:00
events , err := c . history . Load ( room , limit )
2018-03-18 16:34:42 +01:00
if err != nil {
2019-04-05 22:44:17 +02:00
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 )
2019-06-15 00:11:51 +02:00
room . PrevBatch = resp . End
c . config . Rooms . Put ( room )
2019-06-17 11:27:31 +02:00
if len ( resp . Chunk ) == 0 {
return [ ] * event . Event { } , nil
}
events , err = c . history . Prepend ( room , resp . Chunk )
if err != nil {
return nil , err
}
return events , nil
2019-04-05 22:44:17 +02:00
}
2019-06-17 11:27:31 +02:00
func ( c * Container ) GetEvent ( room * rooms . Room , eventID string ) ( * event . Event , error ) {
evt , err := c . history . Get ( room , eventID )
2019-06-19 21:27:48 +02:00
if err != nil && err != EventNotFoundError {
debug . Printf ( "Failed to get event %s from local cache: %v" , eventID , err )
} else if evt != nil {
2019-04-05 22:44:17 +02:00
debug . Printf ( "Found event %s in local cache" , eventID )
2019-06-17 11:27:31 +02:00
return evt , err
2019-04-05 22:44:17 +02:00
}
2019-06-17 11:27:31 +02:00
mxEvent , err := c . client . GetEvent ( room . ID , eventID )
2019-04-05 22:44:17 +02:00
if err != nil {
return nil , err
2018-03-18 16:34:42 +01:00
}
2019-06-17 11:27:31 +02:00
evt = event . Wrap ( mxEvent )
2019-04-05 22:44:17 +02:00
debug . Printf ( "Loaded event %s from server" , eventID )
2019-06-17 11:27:31 +02:00
return evt , nil
2018-03-18 16:34:42 +01:00
}
2019-06-15 00:11:51 +02:00
// GetOrCreateRoom gets the room instance stored in the session.
func ( c * Container ) GetOrCreateRoom ( roomID string ) * rooms . Room {
return c . config . Rooms . GetOrCreate ( roomID )
}
2018-03-21 22:29:58 +01:00
// GetRoom gets the room instance stored in the session.
2018-03-18 20:24:03 +01:00
func ( c * Container ) GetRoom ( roomID string ) * rooms . Room {
2019-06-15 00:11:51 +02:00
return c . config . Rooms . Get ( roomID )
2018-03-14 23:14:39 +01:00
}
2018-04-10 18:31:28 +02:00
var mxcRegex = regexp . MustCompile ( "mxc://(.+)/(.+)" )
2018-05-01 18:17:57 +02:00
// 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.
2018-04-11 18:20:40 +02:00
func ( c * Container ) Download ( mxcURL string ) ( data [ ] byte , hs , id string , err error ) {
2018-04-10 18:31:28 +02:00
parts := mxcRegex . FindStringSubmatch ( mxcURL )
if parts == nil || len ( parts ) != 3 {
2018-04-11 16:57:15 +02:00
err = fmt . Errorf ( "invalid matrix content URL" )
return
2018-04-10 18:31:28 +02:00
}
2018-04-15 13:03:05 +02:00
2018-04-11 18:20:40 +02:00
hs = parts [ 1 ]
id = parts [ 2 ]
2018-04-10 18:31:28 +02:00
2018-04-11 18:20:40 +02:00
cacheFile := c . GetCachePath ( hs , id )
2018-05-10 19:31:11 +02:00
var info os . FileInfo
if info , err = os . Stat ( cacheFile ) ; err == nil && ! info . IsDir ( ) {
2018-04-11 16:57:15 +02:00
data , err = ioutil . ReadFile ( cacheFile )
2018-04-10 18:31:28 +02:00
if err == nil {
2018-04-11 16:57:15 +02:00
return
2018-04-10 18:31:28 +02:00
}
}
2019-06-16 14:19:42 +02:00
data , err = c . download ( hs , id , cacheFile )
2018-04-15 13:03:05 +02:00
return
}
2018-05-22 21:06:48 +02:00
func ( c * Container ) GetDownloadURL ( hs , id string ) string {
2018-04-10 18:31:28 +02:00
dlURL , _ := url . Parse ( c . client . HomeserverURL . String ( ) )
2019-06-16 16:14:51 +02:00
if dlURL . Scheme == "" {
dlURL . Scheme = "https"
}
2018-04-10 18:31:28 +02:00
dlURL . Path = path . Join ( dlURL . Path , "/_matrix/media/v1/download" , hs , id )
2018-05-22 21:06:48 +02:00
return dlURL . String ( )
}
2018-04-10 18:31:28 +02:00
2018-05-22 21:06:48 +02:00
func ( c * Container ) download ( hs , id , cacheFile string ) ( data [ ] byte , err error ) {
2018-04-11 16:57:15 +02:00
var resp * http . Response
2018-05-22 21:06:48 +02:00
resp , err = c . client . Client . Get ( c . GetDownloadURL ( hs , id ) )
2018-04-10 18:31:28 +02:00
if err != nil {
2018-04-11 16:57:15 +02:00
return
2018-04-10 18:31:28 +02:00
}
defer resp . Body . Close ( )
var buf bytes . Buffer
_ , err = io . Copy ( & buf , resp . Body )
if err != nil {
2018-04-11 16:57:15 +02:00
return
2018-04-10 18:31:28 +02:00
}
2018-04-11 16:57:15 +02:00
data = buf . Bytes ( )
2018-04-10 18:31:28 +02:00
err = ioutil . WriteFile ( cacheFile , data , 0600 )
2018-04-11 16:57:15 +02:00
return
2018-04-10 18:31:28 +02:00
}
2018-04-11 18:20:40 +02:00
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.
2018-04-11 18:20:40 +02:00
func ( c * Container ) GetCachePath ( homeserver , fileID string ) string {
2018-04-10 18:31:28 +02:00
dir := filepath . Join ( c . config . MediaDir , homeserver )
err := os . MkdirAll ( dir , 0700 )
if err != nil {
return ""
}
return filepath . Join ( dir , fileID )
}