diff --git a/bridge/config/config.go b/bridge/config/config.go index 67a7dc13..7f8d3a4a 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -154,7 +154,7 @@ type Protocol struct { UseTLS bool // IRC UseDiscriminator bool // discord UseFirstName bool // telegram - UseUserName bool // discord + UseUserName bool // discord, matrix UseInsecureURL bool // telegram VerboseJoinPart bool // IRC WebhookBindAddress string // mattermost, slack diff --git a/bridge/matrix/helpers.go b/bridge/matrix/helpers.go new file mode 100644 index 00000000..91aea805 --- /dev/null +++ b/bridge/matrix/helpers.go @@ -0,0 +1,166 @@ +package bmatrix + +import ( + "encoding/json" + "errors" + "html" + "strings" + "time" + + matrix "github.com/matrix-org/gomatrix" +) + +func newMatrixUsername(username string) *matrixUsername { + mUsername := new(matrixUsername) + + // check if we have a . if we have, we don't escape HTML. #696 + if htmlTag.MatchString(username) { + mUsername.formatted = username + // remove the HTML formatting for beautiful push messages #1188 + mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "") + } else { + mUsername.formatted = html.EscapeString(username) + mUsername.plain = username + } + + return mUsername +} + +// getRoomID retrieves a matching room ID from the channel name. +func (b *Bmatrix) getRoomID(channel string) string { + b.RLock() + defer b.RUnlock() + for ID, name := range b.RoomMap { + if name == channel { + return ID + } + } + + return "" +} + +// interface2Struct marshals and immediately unmarshals an interface. +// Useful for converting map[string]interface{} to a struct. +func interface2Struct(in interface{}, out interface{}) error { + jsonObj, err := json.Marshal(in) + if err != nil { + return err //nolint:wrapcheck + } + + return json.Unmarshal(jsonObj, out) +} + +// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache. +func (b *Bmatrix) getDisplayName(mxid string) string { + if b.GetBool("UseUserName") { + return mxid[1:] + } + + b.RLock() + if val, present := b.NicknameMap[mxid]; 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) + } + + if err != nil { + return b.cacheDisplayName(mxid, mxid[1:]) + } + + return b.cacheDisplayName(mxid, displayName.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 { + now := time.Now() + + // scan to delete old entries, to stop memory usage from becoming too high with old entries + toDelete := []string{} + b.RLock() + for k, v := range b.NicknameMap { + if now.Sub(v.lastUpdated) > 10*time.Minute { + toDelete = append(toDelete, k) + } + } + b.RUnlock() + + b.Lock() + for _, v := range toDelete { + delete(b.NicknameMap, v) + } + b.NicknameMap[mxid] = NicknameCacheEntry{ + displayName: displayName, + lastUpdated: now, + } + b.Unlock() + + return displayName +} + +// handleError converts errors into httpError. +//nolint:exhaustivestruct +func handleError(err error) *httpError { + var mErr matrix.HTTPError + if !errors.As(err, &mErr) { + return &httpError{ + Err: "not a HTTPError", + } + } + + var httpErr httpError + + if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil { + return &httpError{ + Err: "unmarshal failed", + } + } + + return &httpErr +} + +func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { + // Skip empty messages + if content["msgtype"] == nil { + return false + } + + // Only allow image,video or file msgtypes + if !(content["msgtype"].(string) == "m.image" || + content["msgtype"].(string) == "m.video" || + content["msgtype"].(string) == "m.file") { + return false + } + + return true +} + +// 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) + if err != nil { + b.Log.Errorf("getAvatarURL failed: %s", err) + + return "" + } + + url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/") + if url != "" { + url += "?width=37&height=37&method=crop" + } + + return url +} diff --git a/bridge/matrix/matrix.go b/bridge/matrix/matrix.go index fa2a3f80..725f49a6 100644 --- a/bridge/matrix/matrix.go +++ b/bridge/matrix/matrix.go @@ -2,9 +2,7 @@ package bmatrix import ( "bytes" - "encoding/json" "fmt" - "html" "mime" "regexp" "strings" @@ -22,10 +20,16 @@ var ( htmlReplacementTag = regexp.MustCompile("<[^>]*>") ) +type NicknameCacheEntry struct { + displayName string + lastUpdated time.Time +} + type Bmatrix struct { - mc *matrix.Client - UserID string - RoomMap map[string]string + mc *matrix.Client + UserID string + NicknameMap map[string]NicknameCacheEntry + RoomMap map[string]string sync.RWMutex *bridge.Config } @@ -41,25 +45,29 @@ type matrixUsername struct { formatted string } -func newMatrixUsername(username string) *matrixUsername { - mUsername := new(matrixUsername) +// SubTextMessage represents the new content of the message in edit messages. +type SubTextMessage struct { + MsgType string `json:"msgtype"` + Body string `json:"body"` +} - // check if we have a . if we have, we don't escape HTML. #696 - if htmlTag.MatchString(username) { - mUsername.formatted = username - // remove the HTML formatting for beautiful push messages #1188 - mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "") - } else { - mUsername.formatted = html.EscapeString(username) - mUsername.plain = username - } +// 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"` +} - return mUsername +type EditedMessage struct { + NewContent SubTextMessage `json:"m.new_content"` + RelatedTo MessageRelation `json:"m.relates_to"` + matrix.TextMessage } func New(cfg *bridge.Config) bridge.Bridger { b := &Bmatrix{Config: cfg} b.RoomMap = make(map[string]string) + b.NicknameMap = make(map[string]NicknameCacheEntry) return b } @@ -112,22 +120,6 @@ retry: return nil } -type SubTextMessage struct { - MsgType string `json:"msgtype"` - Body string `json:"body"` -} - -type MessageRelation struct { - EventID string `json:"event_id"` - Type string `json:"rel_type"` -} - -type EditedMessage struct { - NewContent SubTextMessage `json:"m.new_content"` - RelatedTo MessageRelation `json:"m.relates_to"` - matrix.TextMessage -} - func (b *Bmatrix) Send(msg config.Message) (string, error) { b.Log.Debugf("=> Receiving %#v", msg) @@ -233,21 +225,11 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { return resp.EventID, err } -func (b *Bmatrix) getRoomID(channel string) string { - b.RLock() - defer b.RUnlock() - for ID, name := range b.RoomMap { - if name == channel { - return ID - } - } - return "" -} - 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) go func() { for { if err := b.mc.Sync(); err != nil { @@ -257,15 +239,6 @@ func (b *Bmatrix) handlematrix() { }() } -func interface2Struct(in interface{}, out interface{}) error { - jsonObj, err := json.Marshal(in) - if err != nil { - return err - } - - return json.Unmarshal(jsonObj, out) -} - 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"] @@ -296,6 +269,15 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool { return true } +func (b *Bmatrix) handleMemberChange(ev *matrix.Event) { + // 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) + } + } +} + func (b *Bmatrix) handleEvent(ev *matrix.Event) { b.Log.Debugf("== Receiving event: %#v", ev) if ev.Sender != b.UserID { @@ -309,7 +291,7 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { // Create our message rmsg := config.Message{ - Username: ev.Sender[1:], + Username: b.getDisplayName(ev.Sender), Channel: channel, Account: b.Account, UserID: ev.Sender, @@ -494,58 +476,3 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf } b.Log.Debugf("result: %#v", res) } - -// skipMessages returns true if this message should not be handled -func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { - // Skip empty messages - if content["msgtype"] == nil { - return false - } - - // Only allow image,video or file msgtypes - if !(content["msgtype"].(string) == "m.image" || - content["msgtype"].(string) == "m.video" || - content["msgtype"].(string) == "m.file") { - return false - } - return true -} - -// 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) - if err != nil { - b.Log.Errorf("getAvatarURL failed: %s", err) - return "" - } - url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/") - if url != "" { - url += "?width=37&height=37&method=crop" - } - return url -} - -func handleError(err error) *httpError { - mErr, ok := err.(matrix.HTTPError) - if !ok { - return &httpError{ - Err: "not a HTTPError", - } - } - - var httpErr httpError - - if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil { - return &httpError{ - Err: "unmarshal failed", - } - } - - return &httpErr -} diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 6b38580f..fcaac2a4 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -1231,6 +1231,9 @@ HTMLDisable=false ## RELOADABLE SETTINGS ## Settings below can be reloaded by editing the file +# UseUserName shows the username instead of the server nickname +UseUserName=false + #Whether to prefix messages from other bridges to matrix with the sender's nick. #Useful if username overrides for incoming webhooks isn't enabled on the #matrix server. If you set PrefixMessagesWithNick to true, each message