Thumbnail

rani/matterbridge.git

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

Viewing file on branch master

1package bmatrix
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "image"
9 "io"
10 "mime"
11 "net/http"
12 "regexp"
13 "strings"
14 "sync"
15 "time"
16
17 // Initialize specific format decoders,
18 // see https://pkg.go.dev/image
19 _ "image/gif"
20 _ "image/jpeg"
21 _ "image/png"
22
23 "github.com/matterbridge-org/matterbridge/bridge"
24 "github.com/matterbridge-org/matterbridge/bridge/config"
25 "github.com/matterbridge-org/matterbridge/bridge/helper"
26
27 mautrix "maunium.net/go/mautrix"
28 "maunium.net/go/mautrix/crypto/cryptohelper"
29 "maunium.net/go/mautrix/event"
30 "maunium.net/go/mautrix/id"
31)
32
33var (
34 htmlTag = regexp.MustCompile("</.*?>")
35 htmlReplacementTag = regexp.MustCompile("<[^>]*>")
36)
37
38type NicknameCacheEntry struct {
39 displayName string
40 lastUpdated time.Time
41}
42
43type Bmatrix struct {
44 mc *mautrix.Client
45 UserID id.UserID
46 NicknameMap map[string]NicknameCacheEntry
47 RoomMap map[id.RoomID]string
48 rateMutex sync.RWMutex
49 sync.RWMutex
50 *bridge.Config
51}
52
53type httpError struct {
54 Errcode string `json:"errcode"`
55 Err string `json:"error"`
56 RetryAfterMs int `json:"retry_after_ms"`
57}
58
59type matrixUsername struct {
60 plain string
61 formatted string
62}
63
64// SubTextMessage represents the new content of the message in edit messages.
65type SubTextMessage struct {
66 MsgType string `json:"msgtype"`
67 Body string `json:"body"`
68 FormattedBody string `json:"formatted_body,omitempty"`
69 Format string `json:"format,omitempty"`
70}
71
72// MessageRelation explains how the current message relates to a previous message.
73// Notably used for message edits.
74type MessageRelation struct {
75 EventID string `json:"event_id"`
76 Type event.MessageType `json:"rel_type"`
77}
78
79type EditedMessage struct {
80 event.MessageEventContent
81
82 NewContent SubTextMessage `json:"m.new_content"`
83 RelatedTo MessageRelation `json:"m.relates_to"`
84}
85
86type InReplyToRelationContent struct {
87 EventID string `json:"event_id"`
88}
89
90type InReplyToRelation struct {
91 InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
92}
93
94type ReplyMessage struct {
95 event.MessageEventContent
96
97 RelatedTo InReplyToRelation `json:"m.relates_to"`
98}
99
100func New(cfg *bridge.Config) bridge.Bridger {
101 b := &Bmatrix{Config: cfg}
102 b.RoomMap = make(map[id.RoomID]string)
103 b.NicknameMap = make(map[string]NicknameCacheEntry)
104 return b
105}
106
107func (b *Bmatrix) Connect() error {
108 var err error
109 b.Log.Infof("Connecting %s", b.GetString("Server"))
110
111 if b.GetString("MxID") != "" && b.GetString("Token") != "" && b.GetString("DeviceID") != "" {
112 userID := id.UserID(b.GetString("MxID"))
113
114 b.mc, err = mautrix.NewClient(
115 b.GetString("Server"), userID, b.GetString("Token"),
116 )
117 if err != nil {
118 return err
119 }
120
121 b.UserID = userID
122 b.Log.Info("Using existing Matrix credentials")
123
124 b.mc.DeviceID = id.DeviceID(b.GetString("DeviceID"))
125 } else {
126 b.mc, err = mautrix.NewClient(b.GetString("Server"), "", "")
127 if err != nil {
128 return err
129 }
130
131 resp, err2 := b.mc.Login(
132 context.TODO(),
133 &mautrix.ReqLogin{
134 Type: mautrix.AuthTypePassword,
135 Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: b.GetString("Login")},
136 Password: b.GetString("Password"),
137 StoreCredentials: true,
138 },
139 )
140 if err2 != nil {
141 return err2
142 }
143 b.UserID = resp.UserID
144 }
145
146 b.Log.Info("Connection succeeded")
147
148 b.Log.Infof("MxID: %s", b.mc.UserID)
149 b.Log.Infof("Token: %s", b.mc.AccessToken)
150 b.Log.Infof("Device ID: %s", b.mc.DeviceID)
151
152 go b.handlematrix()
153 return nil
154}
155
156func (b *Bmatrix) Disconnect() error {
157 return nil
158}
159
160func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
161 return b.retry(func() error {
162 resp, err := b.mc.JoinRoom(context.TODO(), channel.Name, nil)
163 if err != nil {
164 return err
165 }
166
167 b.Lock()
168 b.RoomMap[resp.RoomID] = channel.Name
169 b.Unlock()
170
171 return nil
172 })
173}
174
175// Incoming messages from other bridges
176func (b *Bmatrix) Send(msg config.Message) (string, error) {
177 b.Log.Debugf("=> Receiving %#v", msg)
178
179 roomID := b.getRoomID(msg.Channel)
180 b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, roomID.String())
181
182 username := newMatrixUsername(msg.Username)
183
184 body := username.plain + msg.Text
185 formattedBody := username.formatted + helper.ParseMarkdown(msg.Text)
186
187 if b.GetBool("SpoofUsername") {
188 // https://spec.matrix.org/v1.3/client-server-api/#mroommember
189 type stateMember struct {
190 AvatarURL string `json:"avatar_url,omitempty"`
191 DisplayName string `json:"displayname"`
192 Membership event.Membership `json:"membership"`
193 }
194
195 // TODO: reset username afterwards with DisplayName: null ?
196 content := stateMember{
197 AvatarURL: "",
198 DisplayName: username.plain,
199 Membership: event.MembershipJoin,
200 }
201
202 _, err := b.mc.SendStateEvent(context.TODO(), roomID, event.StateMember, b.UserID.String(), content)
203 if err == nil {
204 body = msg.Text
205 formattedBody = helper.ParseMarkdown(msg.Text)
206 }
207 }
208
209 // Make a action /me of the message
210 if msg.Event == config.EventUserAction {
211 content := event.MessageEventContent{
212 MsgType: event.MsgEmote,
213 Body: body,
214 FormattedBody: formattedBody,
215 Format: event.FormatHTML,
216 }
217
218 if b.GetBool("HTMLDisable") {
219 content.Format = ""
220 content.FormattedBody = ""
221 }
222
223 var msgID id.EventID
224
225 err := b.retry(func() error {
226 resp, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
227 if err != nil {
228 return err
229 }
230
231 msgID = resp.EventID
232
233 return err
234 })
235
236 return msgID.String(), err
237 }
238
239 // Delete message
240 if msg.Event == config.EventMsgDelete {
241 if msg.ID == "" {
242 return "", nil
243 }
244
245 var msgID id.EventID
246
247 err := b.retry(func() error {
248 resp, err := b.mc.RedactEvent(context.TODO(), roomID, id.EventID(msg.ID), mautrix.ReqRedact{})
249 if err != nil {
250 return err
251 }
252
253 msgID = resp.EventID
254
255 return err
256 })
257
258 return msgID.String(), err
259 }
260
261 // Upload a file if it exists
262 if msg.Extra != nil {
263 for _, rmsg := range helper.HandleExtra(&msg, b.General) {
264
265 err := b.retry(func() error {
266 _, err := b.mc.SendText(context.TODO(), roomID, rmsg.Username+rmsg.Text)
267
268 return err
269 })
270 if err != nil {
271 b.Log.Errorf("sendText failed: %s", err)
272 }
273 }
274 // check if we have files to upload (from slack, telegram or mattermost)
275 if len(msg.Extra["file"]) > 0 {
276 return b.handleUploadFiles(&msg, roomID)
277 }
278 }
279
280 // Edit message if we have an ID
281 if msg.ID != "" {
282 content := event.MessageEventContent{
283 Body: body,
284 FormattedBody: formattedBody,
285 MsgType: event.MsgText,
286 Format: event.FormatHTML,
287 NewContent: &event.MessageEventContent{
288 Body: body,
289 FormattedBody: formattedBody,
290 Format: event.FormatHTML,
291 MsgType: event.MsgText,
292 },
293 RelatesTo: &event.RelatesTo{
294 EventID: id.EventID(msg.ID),
295 Type: event.RelReplace,
296 },
297 }
298
299 if b.GetBool("HTMLDisable") {
300 content.Format = ""
301 content.FormattedBody = ""
302 content.NewContent.Format = ""
303 content.NewContent.FormattedBody = ""
304 }
305
306 err := b.retry(func() error {
307 _, err := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
308
309 return err
310 })
311 if err != nil {
312 return "", err
313 }
314
315 return msg.ID, nil
316 }
317
318 // Use notices to send join/leave events
319 if msg.Event == config.EventJoinLeave {
320 content := event.MessageEventContent{
321 MsgType: event.MsgNotice,
322 Body: body,
323 FormattedBody: formattedBody,
324 Format: event.FormatHTML,
325 }
326
327 if b.GetBool("HTMLDisable") {
328 content.Format = ""
329 content.FormattedBody = ""
330 }
331
332 var (
333 resp *mautrix.RespSendEvent
334 err error
335 )
336
337 err = b.retry(func() error {
338 resp, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
339
340 return err
341 })
342 if err != nil {
343 return "", err
344 }
345
346 return resp.EventID.String(), err
347 }
348
349 // Reply to parent if message has a parent id
350 if msg.ParentValid() {
351 content := event.MessageEventContent{
352 MsgType: event.MsgText,
353 Body: body,
354 FormattedBody: formattedBody,
355 Format: event.FormatHTML,
356 RelatesTo: &event.RelatesTo{
357 Type: "m.reply",
358 InReplyTo: &event.InReplyTo{
359 EventID: id.EventID(msg.ParentID),
360 },
361 },
362 }
363
364 if b.GetBool("HTMLDisable") {
365 content.Format = ""
366 content.FormattedBody = ""
367 }
368
369 var (
370 resp *mautrix.RespSendEvent
371 err error
372 )
373
374 err = b.retry(func() error {
375 resp, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
376
377 return err
378 })
379 if err != nil {
380 return "", err
381 }
382
383 return resp.EventID.String(), err
384 }
385
386 // Send a plain text message if html is disabled
387 if b.GetBool("HTMLDisable") {
388 var (
389 resp *mautrix.RespSendEvent
390 err error
391 )
392
393 err = b.retry(func() error {
394 resp, err = b.mc.SendText(context.TODO(), roomID, body)
395
396 return err
397 })
398 if err != nil {
399 return "", err
400 }
401
402 return resp.EventID.String(), err
403 }
404
405 // Post normal message with HTML support (eg riot.im)
406 var (
407 resp *mautrix.RespSendEvent
408 err error
409 )
410
411 err = b.retry(func() error {
412 content := event.MessageEventContent{
413 MsgType: event.MsgText,
414 Body: body,
415 FormattedBody: formattedBody,
416 Format: event.FormatHTML,
417 }
418
419 resp, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
420
421 return err
422 })
423 if err != nil {
424 return "", err
425 }
426
427 return resp.EventID.String(), err
428}
429
430func (b *Bmatrix) NewHttpRequest(method, uri string, body io.Reader) (*http.Request, error) {
431 req, err := http.NewRequest(method, uri, body)
432 if err != nil {
433 return nil, err
434 }
435
436 req.Header.Add("Authorization", "Bearer "+b.mc.AccessToken)
437
438 return req, nil
439}
440
441func (b *Bmatrix) handlematrix() {
442 var (
443 ch *cryptohelper.CryptoHelper = nil
444 err error
445 )
446
447 if b.GetString("SessionFile") != "" &&
448 b.GetString("PickleKey") != "" {
449 // Use a robust key generation method in production (e.g. from environment variable or key management service)
450 pickleKey := []byte(b.GetString("PickleKey"))
451
452 ch, err = setupEncryptedClientHelper(b.mc, pickleKey, b.GetString("SessionFile"))
453 if err != nil {
454 b.Log.Error(err)
455 } else {
456 b.Log.Info("Encryption subsystem configured and attached.")
457 }
458 }
459
460 syncer := b.mc.Syncer.(*mautrix.DefaultSyncer) //nolint:forcetypeassert // We're only using DefaultSyncer
461
462 readyChan := make(chan bool)
463 var once sync.Once
464
465 // Drop historical messages so they don't get forwarded to other bridges
466 syncer.OnSync(b.mc.DontProcessOldEvents)
467 // Drop unencrypted room connection so we can reconnect encrypted if we are using encryption
468 syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
469 once.Do(func() {
470 if ch != nil && b.GetString("RecoveryKey") != "" {
471 close(readyChan)
472 }
473 })
474
475 return true
476 })
477 syncer.OnEventType(event.EventRedaction, b.handleRedactionEvent)
478 syncer.OnEventType(event.EventMessage, b.handleMessageEvent)
479 syncer.OnEventType(event.StateMember, b.handleMemberChange)
480 go func() {
481 for {
482 if b == nil {
483 return
484 }
485
486 err2 := b.mc.Sync()
487 if err2 != nil {
488 b.Log.Debugf("Sync() returned %v, retrying in 5 seconds...\n", err2)
489 time.Sleep(time.Second * 5)
490
491 continue
492 }
493 }
494 }()
495
496 b.Log.Debug("Waiting for sync to receive first event from an encrypted room...")
497 <-readyChan
498 b.Log.Debug("First sync received")
499
500 err = verifyWithRecoveryKey(context.Background(), ch.Machine(), b.GetString("RecoveryKey"))
501 if err != nil {
502 panic(err)
503 } else {
504 b.Log.Info("Verify with recovery key succeeded")
505 }
506}
507
508func (b *Bmatrix) handleEdit(ev *event.Event, rmsg config.Message) bool {
509 relation := ev.Content.AsMessage().OptionalGetRelatesTo()
510
511 if relation == nil {
512 return false
513 }
514
515 if ev.Content.AsMessage().NewContent == nil {
516 return false
517 }
518
519 newContent := ev.Content.AsMessage().NewContent
520
521 if relation.Type != event.RelReplace {
522 return false
523 }
524
525 rmsg.ID = relation.EventID.String()
526 rmsg.Text = newContent.Body
527 b.Remote <- rmsg
528
529 return true
530}
531
532func (b *Bmatrix) handleReply(ev *event.Event, rmsg config.Message) bool {
533 relation := ev.Content.AsMessage().OptionalGetRelatesTo()
534
535 if relation == nil {
536 return false
537 }
538
539 body := rmsg.Text
540
541 if !b.GetBool("keepquotedreply") {
542 for strings.HasPrefix(body, "> ") {
543 lineIdx := strings.IndexRune(body, '\n')
544 if lineIdx == -1 {
545 body = ""
546 } else {
547 body = body[(lineIdx + 1):]
548 }
549 }
550 }
551
552 rmsg.Text = body
553
554 rmsg.ParentID = relation.InReplyTo.EventID.String()
555 b.Remote <- rmsg
556
557 return true
558}
559
560func (b *Bmatrix) handleAttachment(ev *event.Event, rmsg config.Message) bool {
561 if !b.containsAttachment(ev.Content) {
562 return false
563 }
564
565 go func() {
566 // File download is processed in the background to avoid stalling
567 err := b.handleDownloadFile(&rmsg, ev.Content)
568 if err != nil {
569 b.Log.Errorf("%#v", err)
570 return
571 }
572
573 b.Remote <- rmsg
574 }()
575
576 return true
577}
578
579func (b *Bmatrix) handleMemberChange(ctx context.Context, ev *event.Event) {
580 b.Log.Debugf("== Receiving member change event: %#v", ev)
581 // Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
582 content := ev.Content.AsMember()
583
584 if content.Membership == event.MembershipJoin {
585 if content.Displayname != "" {
586 b.cacheDisplayName(ev.Sender, ev.Content.AsMember().Displayname)
587 }
588 }
589}
590
591//nolint:funlen // This function is necessarily long because it is an event handler
592func (b *Bmatrix) handleRedactionEvent(ctx context.Context, ev *event.Event) {
593 b.Log.Debugf("== Receiving redaction event: %#v", ev)
594
595 if ev.Sender == b.UserID {
596 return
597 }
598
599 b.RLock()
600 channel, ok := b.RoomMap[ev.RoomID]
601 b.RUnlock()
602
603 if !ok {
604 b.Log.Debugf("Unknown room %s", ev.RoomID)
605 return
606 }
607
608 // Create our message
609 rmsg := config.Message{
610 Username: b.getDisplayName(ctx, ev.Sender),
611 Channel: channel,
612 Account: b.Account,
613 UserID: ev.Sender.String(),
614 ID: ev.ID.String(),
615 Avatar: b.getAvatarURL(ctx, ev.Sender),
616 }
617
618 // Remove homeserver suffix if configured
619 if b.GetBool("NoHomeServerSuffix") {
620 re := regexp.MustCompile(`\s+\(@.*`)
621 rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)
622 }
623
624 // Delete event
625 if ev.Type == event.EventRedaction {
626 rmsg.Event = config.EventMsgDelete
627 rmsg.ID = ev.Redacts.String()
628
629 rmsg.Text = config.EventMsgDelete
630 b.Remote <- rmsg
631
632 return
633 }
634
635 // Text must be a string
636 if rmsg.Text, ok = ev.Content.GetRaw()["body"].(string); !ok {
637 contentBytes, err := json.Marshal(ev)
638 if err != nil {
639 b.Log.Errorf("Error marshalling event content to JSON: %v", err)
640 return
641 }
642
643 eventString := string(contentBytes)
644
645 b.Log.Errorf("Content[body] is not a string: %T\n%#v", ev.Content.GetRaw()["body"], eventString)
646
647 return
648 }
649
650 b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
651
652 b.Remote <- rmsg
653
654 // not crucial, so no ratelimit check here
655 err := b.mc.MarkRead(ctx, ev.RoomID, ev.ID)
656 if err != nil {
657 b.Log.Errorf("couldn't mark message as read %s", err.Error())
658 }
659}
660
661// Outgoing messages to other bridges
662//
663//nolint:funlen // This function is necessarily long because it is an event handler
664func (b *Bmatrix) handleMessageEvent(ctx context.Context, ev *event.Event) {
665 b.Log.Debugf("== Receiving message event: %#v", ev)
666
667 if ev.Sender == b.UserID {
668 return
669 }
670
671 b.RLock()
672 channel, ok := b.RoomMap[ev.RoomID]
673 b.RUnlock()
674
675 if !ok {
676 b.Log.Debugf("Unknown room %s", ev.RoomID)
677 return
678 }
679
680 // Create our message
681 rmsg := config.Message{
682 Username: b.getDisplayName(ctx, ev.Sender),
683 Channel: channel,
684 Account: b.Account,
685 UserID: ev.Sender.String(),
686 ID: ev.ID.String(),
687 Avatar: b.getAvatarURL(ctx, ev.Sender),
688 }
689
690 // Remove homeserver suffix if configured
691 if b.GetBool("NoHomeServerSuffix") {
692 re := regexp.MustCompile(`\s+\(@.*`)
693 rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)
694 }
695
696 // Delete event as a relation
697 if ev.Unsigned.RedactedBecause != nil {
698 rmsg.Event = config.EventMsgDelete
699 rmsg.ID = ev.Unsigned.RedactedBecause.Redacts.String()
700
701 rmsg.Text = config.EventMsgDelete
702 b.Remote <- rmsg
703
704 return
705 }
706
707 // Text must be a string
708 if rmsg.Text, ok = ev.Content.GetRaw()["body"].(string); !ok {
709 contentBytes, err := json.Marshal(ev)
710 if err != nil {
711 b.Log.Errorf("Error marshalling event content to JSON: %v", err)
712 return
713 }
714
715 eventString := string(contentBytes)
716
717 b.Log.Errorf("Content[body] is not a string: %T\n%#v", ev.Content.GetRaw()["body"], eventString)
718
719 return
720 }
721
722 // Do we have a /me action
723 if ev.Content.AsMessage().MsgType == event.MsgEmote {
724 rmsg.Event = config.EventUserAction
725 }
726
727 // Is it an edit?
728 if b.handleEdit(ev, rmsg) {
729 return
730 }
731
732 // Is it a reply?
733 if b.handleReply(ev, rmsg) {
734 return
735 }
736
737 // Do we have an attachment
738 // TODO: does matrix support multiple attachments?
739 if b.handleAttachment(ev, rmsg) {
740 return
741 }
742
743 b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
744
745 b.Remote <- rmsg
746
747 // not crucial, so no ratelimit check here
748 var err = b.mc.MarkRead(ctx, ev.RoomID, ev.ID)
749 if err != nil {
750 b.Log.Errorf("couldn't mark message as read %s", err.Error())
751 }
752}
753
754// handleDownloadFile handles file download
755func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content event.Content) error {
756 var (
757 ok bool
758 url, name, msgtype, mtype string
759 info map[string]interface{}
760 size float64
761 )
762
763 rmsg.Extra = make(map[string][]interface{})
764
765 if url, ok = content.Raw["url"].(string); !ok {
766 return fmt.Errorf("url isn't a %T", url)
767 }
768 // Matrix downloads now have to be authenticated with an access token
769 // See https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3916-authentication-for-media.md
770 // Also see: https://github.com/matterbridge-org/matterbridge/issues/36
771 url = strings.ReplaceAll(url, "mxc://", b.GetString("Server")+"/_matrix/client/v1/media/download/")
772
773 if info, ok = content.Raw["info"].(map[string]any); !ok {
774 return fmt.Errorf("info isn't a %T", info)
775 }
776
777 if size, ok = info["size"].(float64); !ok {
778 return fmt.Errorf("size isn't a %T", size)
779 }
780
781 if name, ok = content.Raw["body"].(string); !ok {
782 return fmt.Errorf("name isn't a %T", name)
783 }
784
785 if msgtype, ok = content.Raw["msgtype"].(string); !ok {
786 return fmt.Errorf("msgtype isn't a %T", msgtype)
787 }
788
789 if mtype, ok = info["mimetype"].(string); !ok {
790 return fmt.Errorf("mtype isn't a %T", mtype)
791 }
792
793 // check if we have an image uploaded without extension
794 if !strings.Contains(name, ".") {
795 if msgtype == "m.image" {
796 mext, _ := mime.ExtensionsByType(mtype)
797 if len(mext) > 0 {
798 name += mext[0]
799 }
800 } else {
801 // just a default .png extension if we don't have mime info
802 name += ".png"
803 }
804 }
805
806 // TODO: add attachment ID?
807 err := b.AddAttachmentFromURL(rmsg, name, "", "", url)
808 if err != nil {
809 return err
810 }
811 return nil
812}
813
814// handleUploadFiles handles native upload of files.
815func (b *Bmatrix) handleUploadFiles(msg *config.Message, roomID id.RoomID) (string, error) {
816 for _, f := range msg.Extra["file"] {
817 if fi, ok := f.(config.FileInfo); ok {
818 b.handleUploadFile(msg, roomID, &fi)
819 }
820 }
821 return "", nil
822}
823
824// handleUploadFile handles native upload of a file.
825//
826//nolint:funlen // This function is necessarily long because it is an event handler
827func (b *Bmatrix) handleUploadFile(msg *config.Message, roomID id.RoomID, fi *config.FileInfo) {
828 username := newMatrixUsername(msg.Username)
829 content := bytes.NewReader(*fi.Data)
830 sp := strings.Split(fi.Name, ".")
831 mtype := mime.TypeByExtension("." + sp[len(sp)-1])
832 // image and video uploads send no username, we have to do this ourself here #715
833 err := b.retry(func() error {
834 content := event.MessageEventContent{
835 MsgType: event.MsgText,
836 Body: username.plain + fi.Comment,
837 FormattedBody: username.formatted + fi.Comment,
838 Format: event.FormatHTML,
839 }
840
841 _, err2 := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
842
843 return err2
844 })
845 if err != nil {
846 b.Log.Errorf("file comment failed: %#v", err)
847 }
848
849 b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
850
851 var res *mautrix.RespMediaUpload
852
853 err = b.retry(func() error {
854 media := mautrix.ReqUploadMedia{
855 Content: content,
856 ContentType: mtype,
857 ContentLength: int64(len(*fi.Data)),
858 }
859
860 var err2 error
861
862 res, err2 = b.mc.UploadMedia(context.TODO(), media)
863
864 return err2
865 })
866
867 if err != nil {
868 b.Log.Errorf("file upload failed: %#v", err)
869 return
870 }
871
872 switch {
873 case strings.Contains(mtype, "video"):
874 b.Log.Debugf("sendVideo %s", res.ContentURI)
875 err = b.retry(func() error {
876 content := event.MessageEventContent{
877 MsgType: event.MsgVideo,
878 FileName: fi.Name,
879 URL: id.ContentURIString(res.ContentURI.String()),
880 }
881
882 _, err2 := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
883
884 return err2
885 })
886 if err != nil {
887 b.Log.Errorf("sendVideo failed: %#v", err)
888 }
889 case strings.Contains(mtype, "image"):
890 b.Log.Debugf("sendImage %s", res.ContentURI)
891
892 cfg, format, err2 := image.DecodeConfig(bytes.NewReader(*fi.Data))
893 if err2 != nil {
894 b.Log.WithError(err2).Errorf("Failed to decode image %s", fi.Name)
895 return
896 }
897
898 b.Log.Debugf("Image format detected: %s (%dx%d)", format, cfg.Width, cfg.Height)
899
900 img := event.MessageEventContent{
901 MsgType: event.MsgImage,
902 Body: fi.Name,
903 URL: id.ContentURIString(res.ContentURI.String()),
904 Info: &event.FileInfo{
905 MimeType: mtype,
906 Size: len(*fi.Data),
907 Width: cfg.Width, // #nosec G115 -- go std will not returned negative size
908 Height: cfg.Height, // #nosec G115 -- go std will not returned negative size
909 },
910 }
911
912 err = b.retry(func() error {
913 _, err = b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, img)
914 return err
915 })
916 if err != nil {
917 b.Log.Errorf("sendImage failed: %#v", err)
918 }
919 default:
920 b.Log.Debugf("sendFile %s", res.ContentURI)
921 err = b.retry(func() error {
922 content := event.MessageEventContent{
923 MsgType: event.MsgFile,
924 FileName: fi.Name,
925 URL: id.ContentURIString(res.ContentURI.String()),
926 Info: &event.FileInfo{
927 MimeType: mtype,
928 Size: len(*fi.Data),
929 },
930 }
931
932 _, err2 := b.mc.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content)
933
934 return err2
935 })
936 if err != nil {
937 b.Log.Errorf("sendFile failed: %#v", err)
938 }
939 }
940 b.Log.Debugf("result: %#v", res)
941}
942