2020-11-22 14:57:41 +00:00
package bmatrix
import (
"encoding/json"
"errors"
2020-11-25 22:51:23 +00:00
"fmt"
2020-11-22 14:57:41 +00:00
"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 ( )
2020-11-25 22:51:23 +00:00
// 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.
2020-11-22 14:57:41 +00:00
toDelete := [ ] string { }
2020-11-25 22:51:23 +00:00
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
}
2020-11-22 14:57:41 +00:00
if now . Sub ( v . lastUpdated ) > 10 * time . Minute {
2020-11-25 22:51:23 +00:00
toDelete = append ( toDelete , mxid )
2020-11-22 14:57:41 +00:00
}
}
2020-11-25 22:51:23 +00:00
if conflict {
displayName = fmt . Sprintf ( "%s (%s)" , displayName , mxid )
}
2020-11-22 14:57:41 +00:00
for _ , v := range toDelete {
delete ( b . NicknameMap , v )
}
2020-11-25 22:51:23 +00:00
2020-11-22 14:57:41 +00:00
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
}
2020-12-06 16:18:25 +00:00
// 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
}
}
}