Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 63 additions & 101 deletions bridge/matrix/helpers.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package bmatrix

import (
"encoding/json"
"errors"
"fmt"
"html"
"strings"
"time"

matrix "github.com/matrix-org/gomatrix"
matrix "maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
)

func newMatrixUsername(username string) *matrixUsername {
Expand All @@ -28,7 +27,7 @@ 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 {
Expand All @@ -40,28 +39,19 @@ func (b *Bmatrix) getRoomID(channel string) string {
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 {
// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
func (b *Bmatrix) getDisplayName(channelID id.RoomID, mxid id.UserID) string {
if b.GetBool("UseUserName") {
return mxid[1:]
return string(mxid)[1:]
}

b.RLock()
if val, present := b.NicknameMap[mxid]; present {
b.RUnlock()
if channel, channelPresent := b.NicknameMap[channelID]; channelPresent {
if val, present := channel[mxid]; present {
b.RUnlock()

return val.displayName
return val.displayName
}
}
b.RUnlock()

Expand All @@ -72,48 +62,54 @@ func (b *Bmatrix) getDisplayName(mxid string) string {
}

if err != nil {
return b.cacheDisplayName(mxid, mxid[1:])
return b.cacheDisplayName(channelID, mxid, string(mxid)[1:])
}

return b.cacheDisplayName(mxid, displayName.DisplayName)
return b.cacheDisplayName(channelID, 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.
// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homeserver.
// Note that old entries are cleaned when this function is called.
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
func (b *Bmatrix) cacheDisplayName(channelID id.RoomID, 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.
// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
toDelete := []string{}
// scan to delete old entries, to stop memory usage from becoming high with obsolete entries.
// In addition, we detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
toDelete := map[id.RoomID]id.UserID{}
conflict := false

b.Lock()
for mxid, 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 {
conflict = true
// 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
}
for channelIDIter, channelEntriesIter := range b.NicknameMap {
for mxidIter, NicknameCacheIter := range channelEntriesIter {
// to prevent username reuse across matrix rooms - or even inside the same room, if a user uses multiple servers -
// append the mxid to the username when there is a conflict
if NicknameCacheIter.displayName == displayName {
conflict = true
// 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.
NicknameCacheIter.displayName = fmt.Sprintf("%s (%s)", displayName, mxidIter)
b.NicknameMap[channelIDIter][mxidIter] = NicknameCacheIter
}

if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, mxid)
if now.Sub(NicknameCacheIter.lastUpdated) > 10*time.Minute {
toDelete[channelIDIter] = mxidIter
}
}
}

for channelIDIter, mxidIter := range toDelete {
delete(b.NicknameMap[channelIDIter], mxidIter)
}

if conflict {
displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
}

for _, v := range toDelete {
delete(b.NicknameMap, v)
if _, channelPresent := b.NicknameMap[channelID]; !channelPresent {
b.NicknameMap[channelID] = make(map[id.UserID]NicknameCacheEntry)
}

b.NicknameMap[mxid] = NicknameCacheEntry{
b.NicknameMap[channelID][mxid] = NicknameCacheEntry{
displayName: displayName,
lastUpdated: now,
}
Expand All @@ -122,77 +118,43 @@ func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
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)
func (b *Bmatrix) getAvatarURL(sender id.UserID) string {
url, err := b.mc.GetAvatarURL(sender)
if err != nil {
b.Log.Errorf("getAvatarURL failed: %s", err)

b.Log.Errorf("Couldn't retrieve the URL of the avatar for MXID %s", sender)
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
return url.String()
}

// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
httpErr := handleError(err)
if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
var mErr matrix.HTTPError
if !errors.As(err, &mErr) {
b.Log.Errorf("Received a non-HTTPError, don't know what to make of it:\n%#v", err)
return 0, false
}

if mErr.RespError.ErrCode != "M_LIMIT_EXCEEDED" {
return 0, false
}

b.Log.Debugf("ratelimited: %s", httpErr.Err)
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
b.Log.Debugf("ratelimited: %s", mErr.RespError.Err)

// fallback to a one-second delay
retryDelayMs := 1000

if retryDelayString, present := mErr.RespError.ExtraData["retry_after_ms"]; present {
if retryDelayInt, correct := retryDelayString.(int); correct && retryDelayInt > retryDelayMs {
retryDelayMs = retryDelayInt
}
}

b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", retryDelayMs/1000)

return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
return time.Duration(retryDelayMs) * time.Millisecond, true
}

// retry function will check if we're ratelimited and retries again when backoff time expired
Expand Down
Loading