Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

Viewing file on branch master

1package bmatrix
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "html"
9 "time"
10
11 mautrix "maunium.net/go/mautrix"
12 /* trunk-ignore(golangci-lint2/typecheck) */
13 "maunium.net/go/mautrix/crypto"
14 "maunium.net/go/mautrix/crypto/cryptohelper"
15 "maunium.net/go/mautrix/event"
16 "maunium.net/go/mautrix/id"
17)
18
19func newMatrixUsername(username string) *matrixUsername {
20 mUsername := new(matrixUsername)
21
22 // check if we have a </tag>. if we have, we don't escape HTML. #696
23 if htmlTag.MatchString(username) {
24 mUsername.formatted = username
25 // remove the HTML formatting for beautiful push messages #1188
26 mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
27 } else {
28 mUsername.formatted = html.EscapeString(username)
29 mUsername.plain = username
30 }
31
32 return mUsername
33}
34
35// getRoomID retrieves a matching room ID from the channel name.
36func (b *Bmatrix) getRoomID(channel string) id.RoomID {
37 b.RLock()
38 defer b.RUnlock()
39 for ID, name := range b.RoomMap {
40 if name == channel {
41 return ID
42 }
43 }
44
45 return ""
46}
47
48// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
49func (b *Bmatrix) getDisplayName(ctx context.Context, mxid id.UserID) string {
50 // Localpart is the user name. Return it if UseUserName is set.
51 if b.GetBool("UseUserName") {
52 return mxid.Localpart()
53 }
54
55 b.RLock()
56
57 if val, present := b.NicknameMap[mxid.Localpart()]; present {
58 b.RUnlock()
59
60 return val.displayName
61 }
62
63 b.RUnlock()
64
65 resp, err := b.mc.GetDisplayName(ctx, mxid)
66 if err != nil {
67 b.Log.Errorf("Retrieving the display name for %s failed: %s", mxid, err)
68
69 // Return the user name since retrieving the display name failed
70 return b.cacheDisplayName(mxid, mxid.Localpart())
71 }
72
73 return b.cacheDisplayName(mxid, resp.DisplayName)
74}
75
76// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.
77// Note that old entries are cleaned when this function is called.
78func (b *Bmatrix) cacheDisplayName(mxid id.UserID, displayName string) string {
79 now := time.Now()
80
81 // scan to delete old entries, to stop memory usage from becoming too high with old entries.
82 // 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.
83 toDelete := []string{}
84 conflict := false
85
86 b.Lock()
87
88 for localpart, v := range b.NicknameMap {
89 // to prevent username reuse across matrix servers - or even on the same server, append
90 // the mxid to the username when there is a conflict
91 if v.displayName == displayName {
92 conflict = true
93 // TODO: it would be nice to be able to rename previous messages from this user.
94 // 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.
95 v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
96 b.NicknameMap[localpart] = v
97 }
98
99 if now.Sub(v.lastUpdated) > 10*time.Minute {
100 toDelete = append(toDelete, localpart)
101 }
102 }
103
104 if conflict {
105 displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
106 }
107
108 for _, v := range toDelete {
109 delete(b.NicknameMap, v)
110 }
111
112 b.NicknameMap[mxid.Localpart()] = NicknameCacheEntry{
113 displayName: displayName,
114 lastUpdated: now,
115 }
116 b.Unlock()
117
118 return displayName
119}
120
121// handleError converts errors into httpError.
122func handleError(err error) *httpError {
123 var mErr mautrix.HTTPError
124 if !errors.As(err, &mErr) {
125 return &httpError{
126 Err: "not a HTTPError",
127 }
128 }
129
130 var httpErr httpError
131
132 err = json.Unmarshal([]byte(mErr.ResponseBody), &httpErr)
133 if err != nil {
134 return &httpError{
135 Err: "unmarshal failed",
136 }
137 }
138
139 return &httpErr
140}
141
142func (b *Bmatrix) containsAttachment(content event.Content) bool {
143 // Skip empty messages
144 if content.AsMessage().MsgType == "" {
145 return false
146 }
147
148 // Only allow image,video or file msgtypes
149 if content.AsMessage().MsgType != event.MsgImage &&
150 content.AsMessage().MsgType != event.MsgVideo &&
151 content.AsMessage().MsgType != event.MsgAudio &&
152 content.AsMessage().MsgType != event.MsgFile {
153 return false
154 }
155
156 return true
157}
158
159// getAvatarURL returns the avatar URL of the specified sender.
160func (b *Bmatrix) getAvatarURL(ctx context.Context, sender id.UserID) string {
161 urlPath, err := b.mc.GetAvatarURL(ctx, sender)
162 if err != nil {
163 b.Log.Errorf("getAvatarURL failed: %s", err)
164
165 return ""
166 }
167
168 url := b.mc.BuildClientURL(urlPath)
169 if url != "" {
170 url += "?width=37&height=37&method=crop"
171 }
172
173 return url
174}
175
176// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
177func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
178 httpErr := handleError(err)
179 if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
180 return 0, false
181 }
182
183 b.Log.Debugf("ratelimited: %s", httpErr.Err)
184 b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
185
186 return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
187}
188
189// retry function will check if we're ratelimited and retries again when backoff time expired
190// returns original error if not 429 ratelimit
191func (b *Bmatrix) retry(f func() error) error {
192 b.rateMutex.Lock()
193 defer b.rateMutex.Unlock()
194
195 for {
196 if err := f(); err != nil {
197 if backoff, ok := b.handleRatelimit(err); ok {
198 time.Sleep(backoff)
199 } else {
200 return err
201 }
202 } else {
203 return nil
204 }
205 }
206}
207
208// Use this function to set up your client for E2EE
209func setupEncryptedClientHelper(client *mautrix.Client, pickleKey []byte, sessionFile string) (*cryptohelper.CryptoHelper, error) {
210 // The cryptohelper manages the creation of the correct CryptoStore and StateStore implementations.
211 ch, err := cryptohelper.NewCryptoHelper(client, pickleKey, sessionFile)
212 if err != nil {
213 // This usually catches errors with opening the database file or key issues.
214 return nil, fmt.Errorf("failed to create crypto helper: %w", err)
215 }
216
217 // Init MUST be called to load data from the database and prepare the internal machine.
218 err = ch.Init(context.Background())
219 if err != nil {
220 return nil, fmt.Errorf("failed to initialize crypto helper: %w", err)
221 }
222
223 // Hook the helper into the client
224 client.Crypto = ch
225
226 return ch, nil
227}
228
229// Verifies your recovery key with Matrix so you can message unimpeded
230func verifyWithRecoveryKey(ctx context.Context, machine *crypto.OlmMachine, recoveryKey string) error {
231 keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
232 if err != nil {
233 return err
234 }
235
236 key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
237 if err != nil {
238 return err
239 }
240
241 err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
242 if err != nil {
243 return err
244 }
245
246 err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
247 if err != nil {
248 return err
249 }
250
251 err = machine.SignOwnMasterKey(ctx)
252
253 return err
254}
255