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 \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) }