diff options
Diffstat (limited to 'teleirc/matterbridge/bridge/matrix/helpers.go')
| -rw-r--r-- | teleirc/matterbridge/bridge/matrix/helpers.go | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/teleirc/matterbridge/bridge/matrix/helpers.go b/teleirc/matterbridge/bridge/matrix/helpers.go new file mode 100644 index 0000000..5a91f74 --- /dev/null +++ b/teleirc/matterbridge/bridge/matrix/helpers.go @@ -0,0 +1,215 @@ +package bmatrix + +import ( + "encoding/json" + "errors" + "fmt" + "html" + "strings" + "time" + + matrix "github.com/matterbridge/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 homeserver 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. + // 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{} + 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 + } + + if now.Sub(v.lastUpdated) > 10*time.Minute { + toDelete = append(toDelete, mxid) + } + } + + if conflict { + displayName = fmt.Sprintf("%s (%s)", displayName, mxid) + } + + 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 +} + +// 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" { + 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) + + return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true +} + +// retry function will check if we're ratelimited and retries again when backoff time expired +// returns original error if not 429 ratelimit +func (b *Bmatrix) retry(f func() error) error { + b.rateMutex.Lock() + defer b.rateMutex.Unlock() + + for { + if err := f(); err != nil { + if backoff, ok := b.handleRatelimit(err); ok { + time.Sleep(backoff) + } else { + return err + } + } else { + return nil + } + } +} |
