|
|
|
@ -4,10 +4,15 @@ import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"database/sql"
|
|
|
|
|
|
|
|
|
|
"github.com/gomarkdown/markdown"
|
|
|
|
|
gomatrix "maunium.net/go/mautrix"
|
|
|
|
|
"maunium.net/go/mautrix/crypto"
|
|
|
|
|
"maunium.net/go/mautrix/event"
|
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
|
|
|
|
|
|
_ "modernc.org/sqlite"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type MatrixDaemon struct {
|
|
|
|
@ -17,6 +22,10 @@ type MatrixDaemon struct {
|
|
|
|
|
userID id.UserID
|
|
|
|
|
tokens map[string]UnverifiedUser // Map of tokens to users
|
|
|
|
|
languages map[id.RoomID]string // Map of roomIDs to language codes
|
|
|
|
|
isEncrypted map[id.RoomID]bool
|
|
|
|
|
cryptoStore *crypto.SQLCryptoStore
|
|
|
|
|
olm *crypto.OlmMachine
|
|
|
|
|
db *sql.DB
|
|
|
|
|
app *appContext
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -27,17 +36,46 @@ type UnverifiedUser struct {
|
|
|
|
|
|
|
|
|
|
type MatrixUser struct {
|
|
|
|
|
RoomID string
|
|
|
|
|
Encrypted bool
|
|
|
|
|
UserID string
|
|
|
|
|
Lang string
|
|
|
|
|
Contact bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MatrixIdentifier struct {
|
|
|
|
|
User string `json:"user"`
|
|
|
|
|
IdentType string `json:"type"`
|
|
|
|
|
func (m *MatrixDaemon) IsEncrypted(roomID id.RoomID) bool {
|
|
|
|
|
return m.isEncrypted[roomID]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MatrixDaemon) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent {
|
|
|
|
|
return &event.EncryptionEventContent{
|
|
|
|
|
Algorithm: id.AlgorithmMegolmV1,
|
|
|
|
|
RotationPeriodMillis: 7 * 24 * 60 * 60 * 1000,
|
|
|
|
|
RotationPeriodMessages: 100,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Users are assumed to only have one common channel with the bot, so we can stub this out.
|
|
|
|
|
func (m *MatrixDaemon) FindSharedRooms(userID id.UserID) []id.RoomID { return []id.RoomID{} }
|
|
|
|
|
|
|
|
|
|
type olmLogger struct {
|
|
|
|
|
app *appContext
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o olmLogger) Error(message string, args ...interface{}) {
|
|
|
|
|
o.app.err.Printf("OLM: "+message+"\n", args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o olmLogger) Warn(message string, args ...interface{}) {
|
|
|
|
|
o.app.info.Printf("OLM: "+message+"\n", args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m MatrixIdentifier) Type() string { return m.IdentType }
|
|
|
|
|
func (o olmLogger) Debug(message string, args ...interface{}) {
|
|
|
|
|
o.app.debug.Printf("OLM: "+message+"\n", args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o olmLogger) Trace(message string, args ...interface{}) {
|
|
|
|
|
o.app.debug.Printf("OLM [TRACE]: "+message+"\n", args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var matrixFilter = gomatrix.Filter{
|
|
|
|
|
Room: gomatrix.RoomFilter{
|
|
|
|
@ -68,6 +106,7 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
|
|
|
|
userID: id.UserID(matrix.Key("user_id").String()),
|
|
|
|
|
tokens: map[string]UnverifiedUser{},
|
|
|
|
|
languages: map[id.RoomID]string{},
|
|
|
|
|
isEncrypted: map[id.RoomID]bool{},
|
|
|
|
|
app: app,
|
|
|
|
|
}
|
|
|
|
|
d.bot, err = gomatrix.NewClient(homeserver, d.userID, token)
|
|
|
|
@ -80,6 +119,19 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
|
|
|
|
if user.Lang != "" {
|
|
|
|
|
d.languages[id.RoomID(user.RoomID)] = user.Lang
|
|
|
|
|
}
|
|
|
|
|
d.isEncrypted[id.RoomID(user.RoomID)] = user.Encrypted
|
|
|
|
|
}
|
|
|
|
|
d.db, err = sql.Open("sqlite", app.config.Section("files").Key("matrix_sql").String())
|
|
|
|
|
olmLog := &olmLogger{app}
|
|
|
|
|
d.cryptoStore = crypto.NewSQLCryptoStore(d.db, "sqlite3", "jfa-go", "jfa-go", []byte("jfa-go"), olmLog)
|
|
|
|
|
err = d.cryptoStore.CreateTables()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
d.olm = crypto.NewOlmMachine(d.bot, olmLog, d.cryptoStore, crypto.StateStore(d))
|
|
|
|
|
err = d.olm.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
@ -108,7 +160,20 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
|
|
|
|
|
func (d *MatrixDaemon) run() {
|
|
|
|
|
d.app.info.Println("Starting Matrix bot daemon")
|
|
|
|
|
syncer := d.bot.Syncer.(*gomatrix.DefaultSyncer)
|
|
|
|
|
syncer.OnEventType(event.NewEventType("m.room.message"), d.handleMessage)
|
|
|
|
|
syncer.OnEventType(event.EventMessage, d.handleMessage)
|
|
|
|
|
syncer.OnEventType(event.EventEncrypted, func(source gomatrix.EventSource, evt *event.Event) {
|
|
|
|
|
decrypted, err := d.olm.DecryptMegolmEvent(evt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
d.app.err.Printf("Failed to decrypt Matrix message: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
d.handleMessage(source, decrypted)
|
|
|
|
|
})
|
|
|
|
|
syncer.OnSync(d.olm.ProcessSyncResponse)
|
|
|
|
|
syncer.OnEventType(event.StateMember, func(source gomatrix.EventSource, evt *event.Event) {
|
|
|
|
|
d.olm.HandleMemberEvent(evt)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// syncer.OnEventType("m.room.member", d.handleMembership)
|
|
|
|
|
if err := d.bot.Sync(); err != nil {
|
|
|
|
|
d.app.err.Printf("Matrix sync failed: %v", err)
|
|
|
|
@ -117,6 +182,7 @@ func (d *MatrixDaemon) run() {
|
|
|
|
|
|
|
|
|
|
func (d *MatrixDaemon) Shutdown() {
|
|
|
|
|
d.bot.StopSync()
|
|
|
|
|
d.db.Close()
|
|
|
|
|
d.Stopped = true
|
|
|
|
|
close(d.ShutdownChannel)
|
|
|
|
|
}
|
|
|
|
@ -170,20 +236,32 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *MatrixDaemon) CreateRoom(userID string) (id.RoomID, error) {
|
|
|
|
|
room, err := d.bot.CreateRoom(&gomatrix.ReqCreateRoom{
|
|
|
|
|
func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bool, err error) {
|
|
|
|
|
var room *gomatrix.RespCreateRoom
|
|
|
|
|
room, err = d.bot.CreateRoom(&gomatrix.ReqCreateRoom{
|
|
|
|
|
Visibility: "private",
|
|
|
|
|
Invite: []id.UserID{id.UserID(userID)},
|
|
|
|
|
Topic: d.app.config.Section("matrix").Key("topic").String(),
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = d.bot.SendStateEvent(room.RoomID, event.StateEncryption, "", &event.EncryptionEventContent{
|
|
|
|
|
Algorithm: id.AlgorithmMegolmV1,
|
|
|
|
|
RotationPeriodMillis: 7 * 24 * 60 * 60 * 1000,
|
|
|
|
|
RotationPeriodMessages: 100,
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
encrypted = true
|
|
|
|
|
} else {
|
|
|
|
|
d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return room.RoomID, nil
|
|
|
|
|
roomID = room.RoomID
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
|
|
|
|
roomID, err := d.CreateRoom(userID)
|
|
|
|
|
roomID, encrypted, err := d.CreateRoom(userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
|
|
|
|
|
return
|
|
|
|
@ -196,8 +274,10 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
|
|
|
|
RoomID: string(roomID),
|
|
|
|
|
UserID: userID,
|
|
|
|
|
Lang: lang,
|
|
|
|
|
Encrypted: encrypted,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
d.isEncrypted[roomID] = encrypted
|
|
|
|
|
_, err = d.bot.SendText(
|
|
|
|
|
roomID,
|
|
|
|
|
d.app.storage.lang.Telegram[lang].Strings.get("matrixStartMessage")+"\n\n"+pin+"\n\n"+
|
|
|
|
@ -211,24 +291,48 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *MatrixDaemon) Send(message *Message, roomID ...string) (err error) {
|
|
|
|
|
func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
|
|
|
|
|
md := ""
|
|
|
|
|
if message.Markdown != "" {
|
|
|
|
|
// Convert images to links
|
|
|
|
|
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
|
|
|
|
|
}
|
|
|
|
|
for _, ident := range roomID {
|
|
|
|
|
|
|
|
|
|
content := event.MessageEventContent{
|
|
|
|
|
MsgType: "m.text",
|
|
|
|
|
Body: message.Text,
|
|
|
|
|
}
|
|
|
|
|
if md != "" {
|
|
|
|
|
_, err = d.bot.SendMessageEvent(id.RoomID(ident), event.NewEventType("m.room.message"), map[string]interface{}{
|
|
|
|
|
"msgtype": "m.text",
|
|
|
|
|
"body": message.Text,
|
|
|
|
|
"formatted_body": md,
|
|
|
|
|
"format": "org.matrix.custom.html",
|
|
|
|
|
}, gomatrix.ReqSendEvent{})
|
|
|
|
|
content.FormattedBody = md
|
|
|
|
|
content.Format = "org.matrix.custom.html"
|
|
|
|
|
}
|
|
|
|
|
for _, user := range users {
|
|
|
|
|
if user.Encrypted {
|
|
|
|
|
err = d.SendEncrypted(&content, user)
|
|
|
|
|
} else {
|
|
|
|
|
_, err = d.bot.SendText(id.RoomID(ident), message.Text)
|
|
|
|
|
_, err = d.bot.SendMessageEvent(id.RoomID(user.RoomID), event.NewEventType("m.room.message"), content, gomatrix.ReqSendEvent{})
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *MatrixDaemon) SendEncrypted(content *event.MessageEventContent, users ...MatrixUser) (err error) {
|
|
|
|
|
for _, user := range users {
|
|
|
|
|
var encrypted *event.EncryptedEventContent
|
|
|
|
|
encrypted, err = d.olm.EncryptMegolmEvent(id.RoomID(user.RoomID), event.EventMessage, content)
|
|
|
|
|
if err == crypto.SessionExpired || err == crypto.SessionNotShared || err == crypto.NoGroupSession {
|
|
|
|
|
err = d.olm.ShareGroupSession(id.RoomID(user.RoomID), []id.UserID{id.UserID(user.UserID)})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
encrypted, err = d.olm.EncryptMegolmEvent(id.RoomID(user.RoomID), event.EventMessage, content)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = d.bot.SendMessageEvent(id.RoomID(user.RoomID), event.EventEncrypted, encrypted)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|