Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

commit 1173f820c273b5ab51cc9bb1f5e4c4f0d6d85c62 Author: Joseph Crowell <joseph.w.crowell@gmail.com> Date: Thu Dec 11 10:46:26 2025 +0000 switch to mautrix for the matrix library diff --git a/bridge/matrix/helpers.go b/bridge/matrix/helpers.go index eeaa5ab..5e2b66e 100644 --- a/bridge/matrix/helpers.go +++ b/bridge/matrix/helpers.go @@ -115 +115 @@  package bmatrix    import ( + "context"   "encoding/json"   "errors"   "fmt"   "html" - "strings"   "time"   - // Custom fork of unmaintained library, needs replacement: - matrix "github.com/matterbridge/gomatrix" + mautrix "maunium.net/go/mautrix" + "maunium.net/go/mautrix/id"  )    func newMatrixUsername(username string) *matrixUsername { @@ -297 +297 @@ func newMatrixUsername(username string) *matrixUsername {  }    // getRoomID retrieves a matching room ID from the channel name. -func (b *Bmatrix) getRoomID(channel string) string { +func (b *Bmatrix) getRoomID(channel string) id.RoomID {   b.RLock()   defer b.RUnlock()   for ID, name := range b.RoomMap { @@ -5335 +5334 @@ func interface2Struct(in interface{}, out interface{}) error {  }    // getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache. -func (b *Bmatrix) getDisplayName(mxid string) string { +func (b *Bmatrix) getDisplayName(mxid id.UserID) string { + // Localpart is the user name. Return it if UseUserName is set.   if b.GetBool("UseUserName") { - return mxid[1:] + return mxid.Localpart()   }     b.RLock() - if val, present := b.NicknameMap[mxid]; present { + if val, present := b.NicknameMap[mxid.Localpart()]; present {   b.RUnlock()     return val.displayName   }   b.RUnlock()   - displayName, err := b.mc.GetDisplayName(mxid) - var httpError *matrix.HTTPError - if errors.As(err, &httpError) { - b.Log.Warnf("Couldn't retrieve the display name for %s", mxid) - } - + resp, err := b.mc.GetDisplayName(context.TODO(), mxid)   if err != nil { - return b.cacheDisplayName(mxid, mxid[1:]) + b.Log.Errorf("Retrieving the display name for %s failed: %s", mxid, err) + + // Return the user name since retrieving the display name failed + return b.cacheDisplayName(mxid, mxid.Localpart())   }   - return b.cacheDisplayName(mxid, displayName.DisplayName) + return b.cacheDisplayName(mxid, resp.DisplayName)  }    // cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.  // Note that old entries are cleaned when this function is called. -func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string { +func (b *Bmatrix) cacheDisplayName(mxid id.UserID, displayName string) string {   now := time.Now()     // scan to delete old entries, to stop memory usage from becoming too high with old entries. @@ -907 +897 @@ func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {   conflict := false     b.Lock() - for mxid, v := range b.NicknameMap { + for localpart, v := range b.NicknameMap {   // to prevent username reuse across matrix servers - or even on the same server, append   // the mxid to the username when there is a conflict   if v.displayName == displayName { @@ -9811 +9711 @@ func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {   // TODO: it would be nice to be able to rename previous messages from this user.   // The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior.   v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid) - b.NicknameMap[mxid] = v + b.NicknameMap[localpart] = v   }     if now.Sub(v.lastUpdated) > 10*time.Minute { - toDelete = append(toDelete, mxid) + toDelete = append(toDelete, localpart)   }   }   @@ -1147 +1137 @@ func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {   delete(b.NicknameMap, v)   }   - b.NicknameMap[mxid] = NicknameCacheEntry{ + b.NicknameMap[mxid.Localpart()] = NicknameCacheEntry{   displayName: displayName,   lastUpdated: now,   } @@ -1277 +1267 @@ func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {  //  //nolint:exhaustivestruct  func handleError(err error) *httpError { - var mErr matrix.HTTPError + var mErr mautrix.HTTPError   if !errors.As(err, &mErr) {   return &httpError{   Err: "not a HTTPError", @@ -1367 +1358 @@ func handleError(err error) *httpError {     var httpErr httpError   - if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil { + err = json.Unmarshal([]byte(mErr.ResponseBody), &httpErr) + if err != nil {   return &httpError{   Err: "unmarshal failed",   } @@ -16221 +16215 @@ func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {  }    // getAvatarURL returns the avatar URL of the specified sender. -func (b *Bmatrix) getAvatarURL(sender string) string { - urlPath := b.mc.BuildURL("profile", sender, "avatar_url") - - s := struct { - AvatarURL string `json:"avatar_url"` - }{} - - err := b.mc.MakeRequest("GET", urlPath, nil, &s) +func (b *Bmatrix) getAvatarURL(sender id.UserID) string { + urlPath, err := b.mc.GetAvatarURL(context.TODO(), sender)   if err != nil {   b.Log.Errorf("getAvatarURL failed: %s", err)     return ""   }   - url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/") + url := b.mc.BuildClientURL(urlPath)   if url != "" {   url += "?width=37&height=37&method=crop"   } diff --git a/bridge/matrix/matrix.go b/bridge/matrix/matrix.go index 301d634..fe1ce4f 100644 --- a/bridge/matrix/matrix.go +++ b/bridge/matrix/matrix.go @@ -26 +28 @@ package bmatrix    import (   "bytes" + "context" + "encoding/json"   "fmt"   "io"   "mime" @@ -1413 +1610 @@ import (   "github.com/matterbridge-org/matterbridge/bridge"   "github.com/matterbridge-org/matterbridge/bridge/config"   "github.com/matterbridge-org/matterbridge/bridge/helper" - "image" - // Initialize specific format decoders, - // see https://pkg.go.dev/image - matrix "github.com/matterbridge/gomatrix" - _ "image/gif" - _ "image/jpeg" - _ "image/png" + + mautrix "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id"  )    var ( @@ -3410 +3311 @@ type NicknameCacheEntry struct {  }    type Bmatrix struct { - mc *matrix.Client - UserID string + mc *mautrix.Client + UserID id.UserID + AccessToken string   NicknameMap map[string]NicknameCacheEntry - RoomMap map[string]string + RoomMap map[id.RoomID]string   rateMutex sync.RWMutex   sync.RWMutex   *bridge.Config @@ -6514 +6514 @@ type SubTextMessage struct {  // MessageRelation explains how the current message relates to a previous message.  // Notably used for message edits.  type MessageRelation struct { - EventID string `json:"event_id"` - Type string `json:"rel_type"` + EventID string `json:"event_id"` + Type event.MessageType `json:"rel_type"`  }    type EditedMessage struct {   NewContent SubTextMessage `json:"m.new_content"`   RelatedTo MessageRelation `json:"m.relates_to"` - matrix.TextMessage + event.MessageEventContent  }    type InReplyToRelationContent struct { @@ -8512 +8512 @@ type InReplyToRelation struct {    type ReplyMessage struct {   RelatedTo InReplyToRelation `json:"m.relates_to"` - matrix.TextMessage + event.MessageEventContent  }    func New(cfg *bridge.Config) bridge.Bridger {   b := &Bmatrix{Config: cfg} - b.RoomMap = make(map[string]string) + b.RoomMap = make(map[id.RoomID]string)   b.NicknameMap = make(map[string]NicknameCacheEntry)   return b  } @@ -9833 +98100 @@ func New(cfg *bridge.Config) bridge.Bridger {  func (b *Bmatrix) Connect() error {   var err error   b.Log.Infof("Connecting %s", b.GetString("Server")) +   if b.GetString("MxID") != "" && b.GetString("Token") != "" { - b.mc, err = matrix.NewClient( - b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"), + userID := id.NewUserID(b.GetString("MxID"), b.GetString("Server")) + b.mc, err = mautrix.NewClient( + b.GetString("Server"), userID, b.GetString("Token"),   )   if err != nil {   return err   } - b.UserID = b.GetString("MxID") + b.UserID = userID + b.AccessToken = b.GetString("Token")   b.Log.Info("Using existing Matrix credentials")   } else { - b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") + b.mc, err = mautrix.NewClient(b.GetString("Server"), "", "")   if err != nil {   return err   } - resp, err := b.mc.Login(&matrix.ReqLogin{ - Type: "m.login.password", - User: b.GetString("Login"), - Password: b.GetString("Password"), - Identifier: matrix.NewUserIdentifier(b.GetString("Login")), - }) + resp, err := b.mc.Login( + context.TODO(), + &mautrix.ReqLogin{ + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: b.GetString("Login")}, + Password: b.GetString("Password"), + StoreCredentials: true, + }, + )   if err != nil {   return err   } - b.mc.SetCredentials(resp.UserID, resp.AccessToken)   b.UserID = resp.UserID - b.Log.Info("Connection succeeded") + b.AccessToken = resp.AccessToken   } + /** + // BEGIN CACHED MESSAGES FIX + **/ + var initialSyncComplete = false + accountStore := mautrix.NewAccountDataStore("org.example.mybot.synctoken", b.mc) + b.mc.Store = accountStore + + b.Log.Info("Connection succeeded") + + initialFilter := mautrix.Filter{ + Room: &mautrix.RoomFilter{ + Timeline: &mautrix.FilterPart{ + Limit: 0, // Request zero history messages + }, + }, + } + + // Upload the filter using client.CreateFilter() + filterResponse, err := b.mc.CreateFilter(context.TODO(), &initialFilter) + if err != nil { + b.Log.Fatalf("Failed to create filter: %v", err) + } + filterID := filterResponse.FilterID + + err = b.mc.Store.SaveFilterID(context.Background(), b.UserID, filterID) + if err != nil { + b.Log.Fatalf("Failed to save filter ID to store: %v", err) + } + + err = b.mc.Store.SaveNextBatch(context.TODO(), b.UserID, "") + if err != nil { + b.Log.Fatalf("Failed to save initial sync token: %v", err) + } + + syncer := b.mc.Syncer.(*mautrix.DefaultSyncer) + syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) { + // Check if we are still in the initial sync phase + if !initialSyncComplete { + return + } + }) + + go func() { + for { + // Call SyncWithContext() with *only* the context. + // It will use the FilterID and empty NextBatch token saved in the store. + syncErr := b.mc.SyncWithContext(context.TODO()) + if syncErr != nil { + b.Log.Debugf("Sync() returned %v, retrying in 5 seconds...\n", syncErr) + time.Sleep(time.Second * 5) + continue + } + + if !initialSyncComplete { + initialSyncComplete = true + } + } + }() + /** + // END CACHED MESSAGES FIX + **/ +   go b.handlematrix()   return nil  } @@ -1357 +2027 @@ func (b *Bmatrix) Disconnect() error {    func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {   return b.retry(func() error { - resp, err := b.mc.JoinRoom(channel.Name, "", nil) + resp, err := b.mc.JoinRoom(context.TODO(), channel.Name, nil)   if err != nil {   return err   } @@ -14811 +21512 @@ func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {   })  }   +// Send outgoing messages from Matrix to other platforms  func (b *Bmatrix) Send(msg config.Message) (string, error) {   b.Log.Debugf("=> Receiving %#v", msg)   - channel := b.getRoomID(msg.Channel) - b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) + roomID := b.getRoomID(msg.Channel) + b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, roomID.String())     username := newMatrixUsername(msg.Username)   @@ -16219 +23019 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   if b.GetBool("SpoofUsername") {   // https://spec.matrix.org/v1.3/client-server-api/#mroommember   type stateMember struct { - AvatarURL string `json:"avatar_url,omitempty"` - DisplayName string `json:"displayname"` - Membership string `json:"membership"` + AvatarURL string `json:"avatar_url,omitempty"` + DisplayName string `json:"displayname"` + Membership event.Membership `json:"membership"`   }     // TODO: reset username afterwards with DisplayName: null ? - m := stateMember{ + content := stateMember{   AvatarURL: "",   DisplayName: username.plain, - Membership: "join", + Membership: event.MembershipJoin,   }   - _, err := b.mc.SendStateEvent(channel, "m.room.member", b.UserID, m) + _, err := b.mc.SendStateEvent(context.TODO(), roomID, event.StateMember, b.UserID.String(), content)   if err == nil {   body = msg.Text   formattedBody = helper.ParseMarkdown(msg.Text) @@ -18322 +25122 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {     // Make a action /me of the message   if msg.Event == config.EventUserAction { - m := matrix.TextMessage{ - MsgType: "m.emote", + content := event.MessageEventContent{ + MsgType: event.MsgEmote,   Body: body,   FormattedBody: formattedBody, - Format: "org.matrix.custom.html", + Format: event.FormatHTML,   }     if b.GetBool("HTMLDisable") { - m.Format = "" - m.FormattedBody = "" + content.Format = "" + content.FormattedBody = ""   }   - msgID := "" + var msgID id.EventID     err := b.retry(func() error { - resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) + resp, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)   if err != nil {   return err   } @@ -2087 +2767 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return err   })   - return msgID, err + return msgID.String(), err   }     // Delete message @@ -21710 +28510 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return "", nil   }   - msgID := "" + var msgID id.EventID     err := b.retry(func() error { - resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) + resp, err := b.mc.RedactEvent(context.TODO(), roomID, id.EventID(msg.ID), mautrix.ReqRedact{})   if err != nil {   return err   } @@ -23016 +29815 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return err   })   - return msgID, err + return msgID.String(), err   }     // Upload a file if it exists   if msg.Extra != nil {   for _, rmsg := range helper.HandleExtra(&msg, b.General) { - rmsg := rmsg     err := b.retry(func() error { - _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text) + _, err := b.mc.SendText(context.TODO(), roomID, rmsg.Username+rmsg.Text)     return err   }) @@ -24942 +31638 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   }   // check if we have files to upload (from slack, telegram or mattermost)   if len(msg.Extra["file"]) > 0 { - return b.handleUploadFiles(&msg, channel) + return b.handleUploadFiles(&msg, roomID)   }   }     // Edit message if we have an ID   if msg.ID != "" { - rmsg := EditedMessage{ - TextMessage: matrix.TextMessage{ + content := event.MessageEventContent{ + Body: body, + FormattedBody: formattedBody, + MsgType: event.MsgText, + Format: event.FormatHTML, + NewContent: &event.MessageEventContent{   Body: body, - MsgType: "m.text", - Format: "org.matrix.custom.html",   FormattedBody: formattedBody, + Format: event.FormatHTML, + MsgType: event.MsgText, + }, + RelatesTo: &event.RelatesTo{ + EventID: id.EventID(msg.ID), + Type: event.RelReplace,   }, - } - - rmsg.NewContent = SubTextMessage{ - Body: rmsg.TextMessage.Body, - FormattedBody: rmsg.TextMessage.FormattedBody, - Format: rmsg.TextMessage.Format, - MsgType: "m.text",   }     if b.GetBool("HTMLDisable") { - rmsg.TextMessage.Format = "" - rmsg.TextMessage.FormattedBody = "" - rmsg.NewContent.Format = "" - rmsg.NewContent.FormattedBody = "" - } - - rmsg.RelatedTo = MessageRelation{ - EventID: msg.ID, - Type: "m.replace", + content.Format = "" + content.FormattedBody = "" + content.NewContent.Format = "" + content.NewContent.FormattedBody = ""   }     err := b.retry(func() error { - _, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg) + _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -29725 +36025 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {     // Use notices to send join/leave events   if msg.Event == config.EventJoinLeave { - m := matrix.TextMessage{ - MsgType: "m.notice", + content := event.MessageEventContent{ + MsgType: event.MsgNotice,   Body: body,   FormattedBody: formattedBody, - Format: "org.matrix.custom.html", + Format: event.FormatHTML,   }     if b.GetBool("HTMLDisable") { - m.Format = "" - m.FormattedBody = "" + content.Format = "" + content.FormattedBody = ""   }     var ( - resp *matrix.RespSendEvent + resp *mautrix.RespSendEvent   err error   )     err = b.retry(func() error { - resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m) + resp, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -32337 +38635 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return "", err   }   - return resp.EventID, err + return resp.EventID.String(), err   }   + // Reply to parent if message has a parent id   if msg.ParentValid() { - m := ReplyMessage{ - TextMessage: matrix.TextMessage{ - MsgType: "m.text", - Body: body, - FormattedBody: formattedBody, - Format: "org.matrix.custom.html", + content := event.MessageEventContent{ + MsgType: event.MsgText, + Body: body, + FormattedBody: formattedBody, + Format: event.FormatHTML, + RelatesTo: &event.RelatesTo{ + InReplyTo: &event.InReplyTo{ + EventID: id.EventID(msg.ParentID), + },   },   }     if b.GetBool("HTMLDisable") { - m.TextMessage.Format = "" - m.TextMessage.FormattedBody = "" - } - - m.RelatedTo = InReplyToRelation{ - InReplyTo: InReplyToRelationContent{ - EventID: msg.ParentID, - }, + content.Format = "" + content.FormattedBody = ""   }     var ( - resp *matrix.RespSendEvent + resp *mautrix.RespSendEvent   err error   )     err = b.retry(func() error { - resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m) + resp, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -36117 +42218 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return "", err   }   - return resp.EventID, err + return resp.EventID.String(), err   }   + // Send a plain text message if html is disabled   if b.GetBool("HTMLDisable") {   var ( - resp *matrix.RespSendEvent + resp *mautrix.RespSendEvent   err error   )     err = b.retry(func() error { - resp, err = b.mc.SendText(channel, body) + resp, err = b.mc.SendText(context.TODO(), roomID, body)     return err   }) @@ -37917 +44124 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return "", err   }   - return resp.EventID, err + return resp.EventID.String(), err   }     // Post normal message with HTML support (eg riot.im)   var ( - resp *matrix.RespSendEvent + resp *mautrix.RespSendEvent   err error   )     err = b.retry(func() error { - resp, err = b.mc.SendFormattedText(channel, body, formattedBody) + content := event.MessageEventContent{ + MsgType: event.MsgText, + Body: body, + FormattedBody: formattedBody, + Format: event.FormatHTML, + } + + resp, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -3977 +4667 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {   return "", err   }   - return resp.EventID, err + return resp.EventID.String(), err  }    func (b *Bmatrix) NewHttpRequest(method, uri string, body io.Reader) (*http.Request, error) { @@ -41210 +48110 @@ func (b *Bmatrix) NewHttpRequest(method, uri string, body io.Reader) (*http.Requ  }    func (b *Bmatrix) handlematrix() { - syncer := b.mc.Syncer.(*matrix.DefaultSyncer) - syncer.OnEventType("m.room.redaction", b.handleEvent) - syncer.OnEventType("m.room.message", b.handleEvent) - syncer.OnEventType("m.room.member", b.handleMemberChange) + syncer := b.mc.Syncer.(*mautrix.DefaultSyncer) + syncer.OnEventType(event.EventRedaction, b.handleRedactionEvent) + syncer.OnEventType(event.EventMessage, b.handleMessageEvent) + syncer.OnEventType(event.StateMember, b.handleMemberChange)   go func() {   for {   if b == nil { @@ -42845 +49734 @@ func (b *Bmatrix) handlematrix() {   }()  }   -func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool { - relationInterface, present := ev.Content["m.relates_to"] - newContentInterface, present2 := ev.Content["m.new_content"] - if !(present && present2) { - return false - } +func (b *Bmatrix) handleEdit(ev *event.Event, rmsg config.Message) bool { + relation := ev.Content.AsMessage().OptionalGetRelatesTo()   - var relation MessageRelation - if err := interface2Struct(relationInterface, &relation); err != nil { - b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface) + if relation == nil {   return false   }   - var newContent SubTextMessage - if err := interface2Struct(newContentInterface, &newContent); err != nil { - b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface) + if ev.Content.AsMessage().NewContent == nil {   return false   }   - if relation.Type != "m.replace" { + newContent := ev.Content.AsMessage().NewContent + + if relation.Type != event.RelReplace {   return false   }   - rmsg.ID = relation.EventID + rmsg.ID = relation.EventID.String()   rmsg.Text = newContent.Body   b.Remote <- rmsg     return true  }   -func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool { - relationInterface, present := ev.Content["m.relates_to"] - if !present { - return false - } +func (b *Bmatrix) handleReply(ev *event.Event, rmsg config.Message) bool { + relation := ev.Content.AsMessage().OptionalGetRelatesTo()   - var relation InReplyToRelation - if err := interface2Struct(relationInterface, &relation); err != nil { - // probably fine + if relation == nil {   return false   }   @@ -48420 +54220 @@ func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {   }     rmsg.Text = body - rmsg.ParentID = relation.InReplyTo.EventID + rmsg.ParentID = relation.InReplyTo.EventID.String()   b.Remote <- rmsg     return true  }   -func (b *Bmatrix) handleAttachment(ev *matrix.Event, rmsg config.Message) bool { - if !b.containsAttachment(ev.Content) { +func (b *Bmatrix) handleAttachment(ev *event.Event, rmsg config.Message) bool { + if !b.containsAttachment(ev.Content.Raw) {   return false   }     go func() {   // File download is processed in the background to avoid stalling - err := b.handleDownloadFile(&rmsg, ev.Content) + err := b.handleDownloadFile(&rmsg, ev.Content.Raw)   if err != nil {   b.Log.Errorf("%#v", err)   return @@ -50917 +56720 @@ func (b *Bmatrix) handleAttachment(ev *matrix.Event, rmsg config.Message) bool {   return true  }   -func (b *Bmatrix) handleMemberChange(ev *matrix.Event) { +func (b *Bmatrix) handleMemberChange(ctx context.Context, ev *event.Event) { + b.Log.Debugf("== Receiving member change event: %#v", ev)   // Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information - if ev.Content["membership"] == "join" { - if dn, ok := ev.Content["displayname"].(string); ok { - b.cacheDisplayName(ev.Sender, dn) + content := ev.Content.AsMember() + + if content.Membership == event.MembershipJoin { + if content.Displayname != "" { + b.cacheDisplayName(ev.Sender, ev.Content.AsMember().Displayname)   }   }  }   -func (b *Bmatrix) handleEvent(ev *matrix.Event) { - b.Log.Debugf("== Receiving event: %#v", ev) +func (b *Bmatrix) handleRedactionEvent(ctx context.Context, ev *event.Event) { + b.Log.Debugf("== Receiving redaction event: %#v", ev)   if ev.Sender != b.UserID {   b.RLock()   channel, ok := b.RoomMap[ev.RoomID] @@ -53435 +595102 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {   Username: b.getDisplayName(ev.Sender),   Channel: channel,   Account: b.Account, - UserID: ev.Sender, - ID: ev.ID, + UserID: ev.Sender.String(), + ID: ev.ID.String(),   Avatar: b.getAvatarURL(ev.Sender),   }     // Remove homeserver suffix if configured   if b.GetBool("NoHomeServerSuffix") { - re := regexp.MustCompile("(.*?):.*") + re := regexp.MustCompile(`\s+\(@.*`)   rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)   }     // Delete event - if ev.Type == "m.room.redaction" { + if ev.Type == event.EventRedaction {   rmsg.Event = config.EventMsgDelete - rmsg.ID = ev.Redacts + rmsg.ID = ev.Redacts.String()   rmsg.Text = config.EventMsgDelete   b.Remote <- rmsg   return   }     // Text must be a string - if rmsg.Text, ok = ev.Content["body"].(string); !ok { - b.Log.Errorf("Content[body] is not a string: %T\n%#v", - ev.Content["body"], ev.Content) + if rmsg.Text, ok = ev.Content.GetRaw()["body"].(string); !ok { + contentBytes, err := json.Marshal(ev) + if err != nil { + b.Log.Errorf("Error marshalling event content to JSON: %v", err) + return + } + + eventString := string(contentBytes) + + b.Log.Errorf("Content[body] is not a string: %T\n%#v", ev.Content.GetRaw()["body"], eventString) + return + } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account) + b.Remote <- rmsg + + // not crucial, so no ratelimit check here + if err := b.mc.MarkRead(context.TODO(), ev.RoomID, ev.ID); err != nil { + b.Log.Errorf("couldn't mark message as read %s", err.Error()) + } + } +} + +func (b *Bmatrix) handleMessageEvent(ctx context.Context, ev *event.Event) { + b.Log.Debugf("== Receiving message event: %#v", ev) + if ev.Sender != b.UserID { + b.RLock() + channel, ok := b.RoomMap[ev.RoomID] + b.RUnlock() + if !ok { + b.Log.Debugf("Unknown room %s", ev.RoomID) + return + } + + // Create our message + rmsg := config.Message{ + Username: b.getDisplayName(ev.Sender), + Channel: channel, + Account: b.Account, + UserID: ev.Sender.String(), + ID: ev.ID.String(), + Avatar: b.getAvatarURL(ev.Sender), + } + + // Remove homeserver suffix if configured + if b.GetBool("NoHomeServerSuffix") { + re := regexp.MustCompile(`\s+\(@.*`) + rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`) + } + + // Delete event as a relation + if ev.Unsigned.RedactedBecause != nil { + rmsg.Event = config.EventMsgDelete + rmsg.ID = ev.Unsigned.RedactedBecause.Redacts.String() + rmsg.Text = config.EventMsgDelete + b.Remote <- rmsg + return + } + + // Text must be a string + if rmsg.Text, ok = ev.Content.GetRaw()["body"].(string); !ok { + contentBytes, err := json.Marshal(ev) + if err != nil { + b.Log.Errorf("Error marshalling event content to JSON: %v", err) + return + } + + eventString := string(contentBytes) + + b.Log.Errorf("Content[body] is not a string: %T\n%#v", ev.Content.GetRaw()["body"], eventString)   return   }     // Do we have a /me action - if ev.Content["msgtype"].(string) == "m.emote" { + if ev.Content.AsMessage().MsgType == event.MsgEmote {   rmsg.Event = config.EventUserAction   }   @@ -5867 +7147 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {   b.Remote <- rmsg     // not crucial, so no ratelimit check here - if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil { + if err := b.mc.MarkRead(context.TODO(), ev.RoomID, ev.ID); err != nil {   b.Log.Errorf("couldn't mark message as read %s", err.Error())   }   } @@ -64824 +77631 @@ func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]in  }    // handleUploadFiles handles native upload of files. -func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) { +func (b *Bmatrix) handleUploadFiles(msg *config.Message, roomID id.RoomID) (string, error) {   for _, f := range msg.Extra["file"] {   if fi, ok := f.(config.FileInfo); ok { - b.handleUploadFile(msg, channel, &fi) + b.handleUploadFile(msg, roomID, &fi)   }   }   return "", nil  }    // handleUploadFile handles native upload of a file. -func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) { +func (b *Bmatrix) handleUploadFile(msg *config.Message, roomID id.RoomID, fi *config.FileInfo) {   username := newMatrixUsername(msg.Username)   content := bytes.NewReader(*fi.Data)   sp := strings.Split(fi.Name, ".")   mtype := mime.TypeByExtension("." + sp[len(sp)-1])   // image and video uploads send no username, we have to do this ourself here #715   err := b.retry(func() error { - _, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment) + content := event.MessageEventContent{ + MsgType: event.MsgText, + Body: username.plain + fi.Comment, + FormattedBody: username.formatted + fi.Comment, + Format: event.FormatHTML, + } + + _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -67510 +81016 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf     b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)   - var res *matrix.RespMediaUpload + var res *mautrix.RespMediaUpload     err = b.retry(func() error { - res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) + media := mautrix.ReqUploadMedia{ + Content: content, + ContentType: mtype, + ContentLength: int64(len(*fi.Data)), + } + + res, err = b.mc.UploadMedia(context.TODO(), media)     return err   }) @@ -6927 +83313 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf   case strings.Contains(mtype, "video"):   b.Log.Debugf("sendVideo %s", res.ContentURI)   err = b.retry(func() error { - _, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI) + content := event.MessageEventContent{ + MsgType: event.MsgVideo, + FileName: fi.Name, + URL: id.ContentURIString(res.ContentURI.String()), + } + + _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -70129 +84815 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf   }   case strings.Contains(mtype, "image"):   b.Log.Debugf("sendImage %s", res.ContentURI) + err = b.retry(func() error { + content := event.MessageEventContent{ + MsgType: event.MsgImage, + FileName: fi.Name, + URL: id.ContentURIString(res.ContentURI.String()), + }   - cfg, format, err2 := image.DecodeConfig(bytes.NewReader(*fi.Data)) - if err2 != nil { - b.Log.WithError(err2).Errorf("Failed to decode image %s", fi.Name) - return - } - - b.Log.Debugf("Image format detected: %s (%dx%d)", format, cfg.Width, cfg.Height) - - img := matrix.ImageMessage{ - MsgType: "m.image", - Body: fi.Name, - URL: res.ContentURI, - Info: matrix.ImageInfo{ - Mimetype: mtype, - Size: uint(len(*fi.Data)), - Width: uint(cfg.Width), // #nosec G115 -- go std will not returned negative size - Height: uint(cfg.Height), // #nosec G115 -- go std will not returned negative size - }, - } + _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)   - err = b.retry(func() error { - _, err = b.mc.SendMessageEvent(channel, "m.room.message", img)   return err   })   if err != nil { @@ -73215 +86517 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf   case strings.Contains(mtype, "audio"):   b.Log.Debugf("sendAudio %s", res.ContentURI)   err = b.retry(func() error { - _, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{ - MsgType: "m.audio", - Body: fi.Name, - URL: res.ContentURI, - Info: matrix.AudioInfo{ - Mimetype: mtype, - Size: uint(len(*fi.Data)), + content := event.MessageEventContent{ + MsgType: event.MsgAudio, + FileName: fi.Name, + URL: id.ContentURIString(res.ContentURI.String()), + Info: &event.FileInfo{ + MimeType: mtype, + Size: len(*fi.Data),   }, - }) + } + + _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) @@ -75015 +88517 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf   default:   b.Log.Debugf("sendFile %s", res.ContentURI)   err = b.retry(func() error { - _, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{ - MsgType: "m.file", - Body: fi.Name, - URL: res.ContentURI, - Info: matrix.FileInfo{ - Mimetype: mtype, - Size: uint(len(*fi.Data)), + content := event.MessageEventContent{ + MsgType: event.MsgFile, + FileName: fi.Name, + URL: id.ContentURIString(res.ContentURI.String()), + Info: &event.FileInfo{ + MimeType: mtype, + Size: len(*fi.Data),   }, - }) + } + + _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)     return err   }) diff --git a/docs/credits.md b/docs/credits.md index 14c6649..e48bc21 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -107 +107 @@ Matterbridge wouldn't exist without these libraries:  - gozulipbot - <https://github.com/ifo/gozulipbot>  - gumble - <https://github.com/layeh/gumble>  - irc - <https://github.com/lrstanley/girc> -- matrix - <https://github.com/matrix-org/gomatrix> +- matrix - <https://maunium.net/go/mautrix>  - mattermost - <https://github.com/mattermost/mattermost-server>  - msgraph.go - <https://github.com/yaegashi/msgraph.go>  - mumble - <https://github.com/layeh/gumble> diff --git a/go.mod b/go.mod index 19984c3..b044a1a 100644 --- a/go.mod +++ b/go.mod @@ -167 +166 @@ require (   github.com/labstack/echo/v4 v4.12.0   github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03   github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 - github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27   github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75   github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba   github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1 @@ -496 +487 @@ require (   gomod.garykim.dev/nc-talk v0.3.0   google.golang.org/protobuf v1.36.10   layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 + maunium.net/go/mautrix v0.26.0   modernc.org/sqlite v1.32.0  )   @@ -11111 +11116 @@ require (   github.com/sagikazarmark/slog-shim v0.1.0 // indirect   github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect   github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect   github.com/sourcegraph/conc v0.3.0 // indirect   github.com/spf13/afero v1.11.0 // indirect   github.com/spf13/cast v1.6.0 // indirect   github.com/spf13/pflag v1.0.5 // indirect   github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect   github.com/tinylib/msgp v1.2.0 // indirect   github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect   github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index e4381ce..115140d 100644 --- a/go.sum +++ b/go.sum @@ -1868 +1866 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v  github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=  github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 h1:pmPKkN3RJM9wVMZidR99epzK0+gatQiqVtvP1FacZcQ=  github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A= -github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 h1:9XSppnbvvReVom+wphkeF4lbhuT6vCYIdyzpwFtW89c= -github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27/go.mod h1:/x38AoZf70fK9yZ5gs3BNCaF7/J4QEo4ZpwtLjX95eQ=  github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 h1:GslZKF7lW7oSisycGLpxPO+TnKJuA4VZuTWIfYZrClc=  github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA=  github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba h1:XleOY4IjAEIcxAh+IFwT5JT5Ze3RHiYz6m+4ZfZ0rc0= @@ -3436 +3418 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ  github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=  github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 h1:A7o8tOERTtpD/poS+2VoassCjXpjHn916luXbf5QKD0=  github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882/go.mod h1:5IwJoz9Pw7JsrCN4/skkxUtSWT7myuUPLhCgv6Q5vvQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=  github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=  github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=  github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= @@ -3786 +37816 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD  github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=  github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=  github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=  github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY=  github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro=  github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= @@ -5916 +6018 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh  layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0=  layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 h1:wY8eeq7DpM5iAugNbFrvuhdtmN8XM1iU+Ki5YZWjukg=  layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14/go.mod h1:tWPVA9ZAfImNwabjcd9uDE+Mtz0Hfs7a7G3vxrnrwyc= +maunium.net/go/mautrix v0.26.0 h1:valc2VmZF+oIY4bMq4Cd5H9cEKMRe8eP4FM7iiaYLxI= +maunium.net/go/mautrix v0.26.0/go.mod h1:NWMv+243NX/gDrLofJ2nNXJPrG8vzoM+WUCWph85S6Q=  modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=  modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=  modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=