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 } } }