mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
Harvey Tindall
69569e556a
mautrix-go now include a cryptohelper package, which solves all my issues and just works. the setting is now on by default, however packages are not yet built with it.
353 lines
9.3 KiB
Go
353 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gomarkdown/markdown"
|
|
lm "github.com/hrfee/jfa-go/logmessages"
|
|
"github.com/timshannon/badgerhold/v4"
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/event"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
var (
|
|
DEVICE_ID = id.DeviceID("jfa-go")
|
|
)
|
|
|
|
type MatrixDaemon struct {
|
|
Stopped bool
|
|
bot *mautrix.Client
|
|
userID id.UserID
|
|
homeserver string
|
|
tokens map[string]UnverifiedUser // Map of tokens to users
|
|
languages map[id.RoomID]string // Map of roomIDs to language codes
|
|
Encryption bool
|
|
crypto *Crypto
|
|
app *appContext
|
|
start int64
|
|
cancellation sync.WaitGroup
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
type UnverifiedUser struct {
|
|
Verified bool
|
|
User *MatrixUser
|
|
}
|
|
|
|
var matrixFilter = mautrix.Filter{
|
|
Room: mautrix.RoomFilter{
|
|
Timeline: mautrix.FilterPart{
|
|
Types: []event.Type{
|
|
event.EventMessage,
|
|
event.EventEncrypted,
|
|
event.StateMember,
|
|
},
|
|
},
|
|
},
|
|
EventFields: []string{
|
|
"type",
|
|
"event_id",
|
|
"room_id",
|
|
"state_key",
|
|
"sender",
|
|
"content",
|
|
"timestamp",
|
|
// "content.body",
|
|
// "content.membership",
|
|
},
|
|
}
|
|
|
|
func (d *MatrixDaemon) renderUserID(uid id.UserID) id.UserID {
|
|
if uid[0] != '@' {
|
|
uid = "@" + uid
|
|
}
|
|
if !strings.ContainsRune(string(uid), ':') {
|
|
uid = id.UserID(string(uid) + ":" + d.homeserver)
|
|
}
|
|
return uid
|
|
}
|
|
|
|
func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
|
matrix := app.config.Section("matrix")
|
|
token := matrix.Key("token").String()
|
|
d = &MatrixDaemon{
|
|
userID: id.UserID(matrix.Key("user_id").String()),
|
|
homeserver: matrix.Key("homeserver").String(),
|
|
tokens: map[string]UnverifiedUser{},
|
|
languages: map[id.RoomID]string{},
|
|
app: app,
|
|
start: time.Now().UnixNano() / 1e6,
|
|
}
|
|
d.userID = d.renderUserID(d.userID)
|
|
d.bot, err = mautrix.NewClient(d.homeserver, d.userID, token)
|
|
if err != nil {
|
|
return
|
|
}
|
|
d.bot.DeviceID = DEVICE_ID
|
|
// resp, err := d.bot.CreateFilter(&matrixFilter)
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
|
|
for _, user := range app.storage.GetMatrix() {
|
|
if user.Lang != "" {
|
|
d.languages[id.RoomID(user.RoomID)] = user.Lang
|
|
}
|
|
}
|
|
err = InitMatrixCrypto(d)
|
|
return
|
|
}
|
|
|
|
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
|
func (d *MatrixDaemon) SetTransport(t *http.Transport) {
|
|
d.bot.Client.Transport = t
|
|
}
|
|
|
|
func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string) (string, error) {
|
|
req := &mautrix.ReqLogin{
|
|
Type: mautrix.AuthTypePassword,
|
|
Identifier: mautrix.UserIdentifier{
|
|
Type: mautrix.IdentifierTypeUser,
|
|
User: username,
|
|
},
|
|
Password: password,
|
|
DeviceID: DEVICE_ID,
|
|
}
|
|
bot, err := mautrix.NewClient(homeserver, id.UserID(username), "")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resp, err := bot.Login(context.TODO(), req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.AccessToken, nil
|
|
}
|
|
|
|
func (d *MatrixDaemon) run() {
|
|
syncer := d.bot.Syncer.(*mautrix.DefaultSyncer)
|
|
syncer.OnEventType(event.EventMessage, d.handleMessage)
|
|
|
|
d.app.info.Printf(lm.StartDaemon, lm.Matrix)
|
|
|
|
var syncCtx context.Context
|
|
syncCtx, d.cancel = context.WithCancel(context.Background())
|
|
d.cancellation.Add(1)
|
|
|
|
if err := d.bot.SyncWithContext(syncCtx); err != nil && !errors.Is(err, context.Canceled) {
|
|
d.app.err.Printf(lm.FailedSyncMatrix, err)
|
|
}
|
|
d.cancellation.Done()
|
|
}
|
|
|
|
func (d *MatrixDaemon) Shutdown() {
|
|
d.cancel()
|
|
d.cancellation.Wait()
|
|
d.Stopped = true
|
|
}
|
|
|
|
func (d *MatrixDaemon) handleMessage(ctx context.Context, evt *event.Event) {
|
|
if evt.Timestamp < d.start {
|
|
return
|
|
}
|
|
if evt.Sender == d.userID {
|
|
return
|
|
}
|
|
lang := "en-us"
|
|
if l, ok := d.languages[evt.RoomID]; ok {
|
|
if _, ok := d.app.storage.lang.Telegram[l]; ok {
|
|
lang = l
|
|
}
|
|
}
|
|
sects := strings.Split(evt.Content.Raw["body"].(string), " ")
|
|
switch sects[0] {
|
|
case "!lang":
|
|
if len(sects) == 2 {
|
|
d.commandLang(evt, sects[1], lang)
|
|
} else {
|
|
d.commandLang(evt, "", lang)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
|
if code == "" {
|
|
list := "!lang <lang>\n"
|
|
for c := range d.app.storage.lang.Telegram {
|
|
list += fmt.Sprintf("%s: %s\n", c, d.app.storage.lang.Telegram[c].Meta.Name)
|
|
}
|
|
_, err := d.bot.SendText(
|
|
context.TODO(),
|
|
evt.RoomID,
|
|
list,
|
|
)
|
|
if err != nil {
|
|
d.app.err.Printf(lm.FailedReply, lm.Matrix, evt.Sender, err)
|
|
}
|
|
return
|
|
}
|
|
if _, ok := d.app.storage.lang.Telegram[code]; !ok {
|
|
return
|
|
}
|
|
d.languages[evt.RoomID] = code
|
|
if u, ok := d.app.storage.GetMatrixKey(string(evt.RoomID)); ok {
|
|
u.Lang = code
|
|
d.app.storage.SetMatrixKey(string(evt.RoomID), u)
|
|
}
|
|
}
|
|
|
|
func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, err error) {
|
|
var room *mautrix.RespCreateRoom
|
|
room, err = d.bot.CreateRoom(context.TODO(), &mautrix.ReqCreateRoom{
|
|
Visibility: "private",
|
|
Invite: []id.UserID{id.UserID(userID)},
|
|
Topic: d.app.config.Section("matrix").Key("topic").String(),
|
|
IsDirect: true,
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
// encrypted = EncryptRoom(d, room, id.UserID(userID))
|
|
roomID = room.RoomID
|
|
err = EncryptRoom(d, roomID)
|
|
return
|
|
}
|
|
|
|
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
|
roomID, err := d.CreateRoom(userID)
|
|
if err != nil {
|
|
d.app.err.Printf(lm.FailedCreateMatrixRoom, userID, err)
|
|
return
|
|
}
|
|
lang := "en-us"
|
|
pin := genAuthToken()
|
|
d.tokens[pin] = UnverifiedUser{
|
|
false,
|
|
&MatrixUser{
|
|
RoomID: string(roomID),
|
|
UserID: userID,
|
|
Lang: lang,
|
|
},
|
|
}
|
|
err = d.sendToRoom(
|
|
&event.MessageEventContent{
|
|
MsgType: event.MsgText,
|
|
Body: d.app.storage.lang.Telegram[lang].Strings.get("matrixStartMessage") + "\n\n" + pin + "\n\n" +
|
|
d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"}),
|
|
},
|
|
roomID,
|
|
)
|
|
if err != nil {
|
|
d.app.err.Printf(lm.FailedMessage, lm.Matrix, userID, err)
|
|
return
|
|
}
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
func (d *MatrixDaemon) sendToRoom(content *event.MessageEventContent, roomID id.RoomID) (err error) {
|
|
return d.send(content, roomID)
|
|
/*if encrypted, ok := d.isEncrypted[roomID]; ok && encrypted {
|
|
err = SendEncrypted(d, content, roomID)
|
|
} else {
|
|
_, err = d.bot.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content, mautrix.ReqSendEvent{})
|
|
}
|
|
return*/
|
|
}
|
|
|
|
func (d *MatrixDaemon) send(content *event.MessageEventContent, roomID id.RoomID) (err error) {
|
|
_, err = d.bot.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content, mautrix.ReqSendEvent{})
|
|
return
|
|
}
|
|
|
|
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, markdownRenderer))
|
|
}
|
|
content := &event.MessageEventContent{
|
|
MsgType: "m.text",
|
|
Body: message.Text,
|
|
}
|
|
if md != "" {
|
|
content.FormattedBody = md
|
|
content.Format = "org.matrix.custom.html"
|
|
}
|
|
for _, user := range users {
|
|
err = d.sendToRoom(content, id.RoomID(user.RoomID))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// UserExists returns whether or not a user with the given User ID exists.
|
|
func (d *MatrixDaemon) UserExists(userID string) bool {
|
|
c, err := d.app.storage.db.Count(&MatrixUser{}, badgerhold.Where("UserID").Eq(userID))
|
|
return err != nil || c > 0
|
|
}
|
|
|
|
// Exists returns whether or not the given user exists.
|
|
func (d *MatrixDaemon) Exists(user ContactMethodUser) bool {
|
|
return d.UserExists(user.Name())
|
|
}
|
|
|
|
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
|
|
|
// Message the user first, to avoid E2EE by default
|
|
|
|
func (d *MatrixDaemon) PIN(req newUserDTO) string { return req.MatrixPIN }
|
|
|
|
func (d *MatrixDaemon) Name() string { return lm.Matrix }
|
|
|
|
func (d *MatrixDaemon) Required() bool {
|
|
return d.app.config.Section("telegram").Key("required").MustBool(false)
|
|
}
|
|
|
|
func (d *MatrixDaemon) UniqueRequired() bool {
|
|
return d.app.config.Section("telegram").Key("require_unique").MustBool(false)
|
|
}
|
|
|
|
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
|
func (d *MatrixDaemon) TokenVerified(pin string) (token UnverifiedUser, ok bool) {
|
|
token, ok = d.tokens[pin]
|
|
// delete(t.verifiedTokens, pin)
|
|
return
|
|
}
|
|
|
|
// DeleteVerifiedToken removes the token with the given PIN.
|
|
func (d *MatrixDaemon) DeleteVerifiedToken(PIN string) {
|
|
delete(d.tokens, PIN)
|
|
}
|
|
|
|
func (d *MatrixDaemon) UserVerified(PIN string) (ContactMethodUser, bool) {
|
|
token, ok := d.TokenVerified(PIN)
|
|
if !ok {
|
|
return &MatrixUser{}, false
|
|
}
|
|
return token.User, ok
|
|
}
|
|
|
|
func (d *MatrixDaemon) PostVerificationTasks(string, ContactMethodUser) error { return nil }
|
|
|
|
func (m *MatrixUser) Name() string { return m.UserID }
|
|
func (m *MatrixUser) SetMethodID(id any) { m.UserID = id.(string) }
|
|
func (m *MatrixUser) MethodID() any { return m.UserID }
|
|
func (m *MatrixUser) SetJellyfin(id string) { m.JellyfinID = id }
|
|
func (m *MatrixUser) Jellyfin() string { return m.JellyfinID }
|
|
func (m *MatrixUser) SetAllowContactFromDTO(req newUserDTO) { m.Contact = req.MatrixContact }
|
|
func (m *MatrixUser) SetAllowContact(contact bool) { m.Contact = contact }
|
|
func (m *MatrixUser) AllowContact() bool { return m.Contact }
|
|
func (m *MatrixUser) Store(st *Storage) {
|
|
st.SetMatrixKey(m.Jellyfin(), *m)
|
|
}
|