summaryrefslogtreecommitdiffstats
path: root/bridge
diff options
context:
space:
mode:
authorSimon THOBY <git@nightmared.fr>2020-11-22 15:57:41 +0100
committerGitHub <noreply@github.com>2020-11-22 15:57:41 +0100
commit1a3c57a031acfd99f9a6882478f866d249f9d39d (patch)
treed6203d045a025cc9b44ade17710a4b77f3e38e9a /bridge
parent4cc2c914e634eb8c79eb61aa1bc29faf6021ffcf (diff)
downloadmatterbridge-msglm-1a3c57a031acfd99f9a6882478f866d249f9d39d.tar.gz
matterbridge-msglm-1a3c57a031acfd99f9a6882478f866d249f9d39d.tar.bz2
matterbridge-msglm-1a3c57a031acfd99f9a6882478f866d249f9d39d.zip
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
Diffstat (limited to 'bridge')
-rw-r--r--bridge/config/config.go2
-rw-r--r--bridge/matrix/helpers.go166
-rw-r--r--bridge/matrix/matrix.go147
3 files changed, 204 insertions, 111 deletions
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 </tag>. 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)
-
- // check if we have a </tag>. 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
- }
+// SubTextMessage represents the new content of the message in edit messages.
+type SubTextMessage struct {
+ MsgType string `json:"msgtype"`
+ Body string `json:"body"`
+}
+
+// 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
-}