summaryrefslogtreecommitdiffstats
path: root/bridge/matrix
diff options
context:
space:
mode:
Diffstat (limited to 'bridge/matrix')
-rw-r--r--bridge/matrix/helpers.go166
-rw-r--r--bridge/matrix/matrix.go147
2 files changed, 203 insertions, 110 deletions
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
-}