From 1a3c57a031acfd99f9a6882478f866d249f9d39d Mon Sep 17 00:00:00 2001 From: Simon THOBY Date: Sun, 22 Nov 2020 15:57:41 +0100 Subject: [PATCH] Send the display name instead of the user name (matrix) (#1282) * matrix: send the display name (the nickname in matrix parlance) instead of the user name There is also the option UseUserName (already in use by the discord bridge) to turn back to the old behavior. * matrix: update displayNames on join events * matrix: introduce a helper.go file to keep matrix.go size reasonable --- bridge/config/config.go | 2 +- bridge/matrix/helpers.go | 166 +++++++++++++++++++++++++++++++++++++++ bridge/matrix/matrix.go | 145 +++++++++------------------------- matterbridge.toml.sample | 3 + 4 files changed, 206 insertions(+), 110 deletions(-) create mode 100644 bridge/matrix/helpers.go 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