commit 1173f820c273b5ab51cc9bb1f5e4c4f0d6d85c62
Author: Joseph Crowell <joseph.w.crowell@gmail.com>
Date: Thu Dec 11 10:46:26 2025 +0000
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=