summaryrefslogtreecommitdiffstats
path: root/bridge/matrix/handlers.go
diff options
context:
space:
mode:
Diffstat (limited to 'bridge/matrix/handlers.go')
-rw-r--r--bridge/matrix/handlers.go344
1 files changed, 344 insertions, 0 deletions
diff --git a/bridge/matrix/handlers.go b/bridge/matrix/handlers.go
new file mode 100644
index 00000000..8833b0ed
--- /dev/null
+++ b/bridge/matrix/handlers.go
@@ -0,0 +1,344 @@
+package bmatrix
+
+import (
+ "bytes"
+ "fmt"
+ "mime"
+ "regexp"
+ "strings"
+
+ matrix "maunium.net/go/mautrix"
+ "maunium.net/go/mautrix/event"
+ "maunium.net/go/mautrix/id"
+
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/bridge/helper"
+)
+
+// Determines if the event comes from ourselves, in which case we want to ignore it
+func (b *Bmatrix) ignoreBridgingEvents(ev *event.Event) bool {
+ if ev.Sender == b.UserID {
+ return true
+ }
+
+ // ignore messages we may have sent via the appservice
+ if b.appService != nil {
+ if ev.Sender == b.appService.appService.BotClient().UserID {
+ return true
+ }
+
+ // ignore virtual users messages (we ignore the 'exclusive' field of Namespace for now)
+ for _, username := range b.appService.namespaces.usernames {
+ if username.MatchString(ev.Sender.String()) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+//nolint: funlen
+func (b *Bmatrix) handleEvent(origin EventOrigin, ev *event.Event) {
+ if b.ignoreBridgingEvents(ev) {
+ return
+ }
+
+ b.RLock()
+ channel, ok := b.RoomMap[ev.RoomID]
+ b.RUnlock()
+ if !ok {
+ // we don't know that room yet, that could be a room returned by an
+ // application service, but matterbridge doesn't handle those just yet
+ b.Log.Debugf("Received event for room %s, not joined yet/not handled", ev.RoomID)
+
+ return
+ }
+
+ if ev.Type == event.EphemeralEventReceipt {
+ // we do not support read receipts across servers, considering that
+ // multiple services (e.g. Discord) doesn't expose that information)
+ return
+ }
+
+ if ev.Type == event.StateMember {
+ b.handleMemberChange(ev)
+
+ return
+ }
+
+ // if we receive appservice events for this room, there is no need to check them with the classical syncer
+ if !channel.appService && origin == originAppService {
+ channel.appService = true
+ b.Lock()
+ b.RoomMap[ev.RoomID] = channel
+ b.Unlock()
+ }
+
+ // if we receive messages both via the classical matrix syncer and appserver, prefer appservice and throw away this duplicate event
+ if channel.appService && origin != originAppService {
+ b.Log.Debugf("Dropping event, should receive it via appservice: %s", ev.ID)
+
+ return
+ }
+
+ b.Log.Debugf("== Receiving event: %#v (appService=%t)", ev, origin == originAppService)
+
+ if ev.Type == event.EphemeralEventTyping {
+ typing := ev.Content.AsTyping()
+ if len(typing.UserIDs) > 0 {
+ //nolint:exhaustruct
+ b.Remote <- config.Message{
+ Event: config.EventUserTyping,
+ Channel: channel.name,
+ Account: b.Account,
+ }
+ }
+
+ return
+ }
+
+ defer (func(ev *event.Event) {
+ // not crucial, so no ratelimit check here
+ if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
+ b.Log.Errorf("couldn't mark message as read %s", err.Error())
+ }
+ })(ev)
+
+ // Create our message
+ //nolint:exhaustruct
+ rmsg := config.Message{
+ Username: b.getDisplayName(ev.RoomID, ev.Sender),
+ Channel: channel.name,
+ Account: b.Account,
+ UserID: string(ev.Sender),
+ ID: string(ev.ID),
+ }
+
+ // Remove homeserver suffix if configured
+ if b.GetBool("NoHomeServerSuffix") {
+ re := regexp.MustCompile("(.*?):.*")
+ rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)
+ }
+
+ // Delete event
+ if ev.Type == event.EventRedaction {
+ rmsg.Event = config.EventMsgDelete
+ rmsg.ID = string(ev.Redacts)
+ rmsg.Text = config.EventMsgDelete
+ b.Remote <- rmsg
+
+ return
+ }
+
+ b.handleMessage(rmsg, ev)
+}
+
+func (b *Bmatrix) handleMemberChange(ev *event.Event) {
+ member := ev.Content.AsMember()
+ if member == nil {
+ b.Log.Errorf("Couldn't process a member event:\n%#v", ev)
+
+ return
+ }
+
+ // Update the displayname on join messages, according to https://spec.matrix.org/v1.3/client-server-api/#events-on-change-of-profile-information
+ if member.Membership == event.MembershipJoin {
+ b.cacheDisplayName(ev.RoomID, ev.Sender, member.Displayname)
+ } else if member.Membership == event.MembershipLeave || member.Membership == event.MembershipBan {
+ b.removeDisplayNameFromCache(ev.Sender)
+ }
+}
+
+func (b *Bmatrix) handleMessage(rmsg config.Message, ev *event.Event) {
+ msg := ev.Content.AsMessage()
+ if msg == nil {
+ b.Log.Errorf("matterbridge don't support this event type: %s", ev.Type.Type)
+ b.Log.Debugf("Full event: %#v", ev)
+
+ return
+ }
+
+ rmsg.Text = msg.Body
+
+ // TODO: cache the avatars
+ avatarURL := b.getAvatarURL(ev.Sender)
+ contentURI, err := id.ParseContentURI(avatarURL)
+ if err == nil {
+ avatarURL = b.mc.GetDownloadURL(contentURI)
+ rmsg.Avatar = avatarURL
+ }
+
+ // Do we have a /me action
+ //nolint: exhaustive
+ switch msg.MsgType {
+ case event.MsgEmote:
+ rmsg.Event = config.EventUserAction
+ case event.MsgImage, event.MsgVideo, event.MsgFile:
+ // Do we have attachments? (we only allow images, videos or files msgtypes)
+ err := b.handleDownloadFile(&rmsg, *msg)
+ if err != nil {
+ b.Log.Errorf("download failed: %#v", err)
+ }
+ default:
+ if msg.RelatesTo == nil {
+ break
+ }
+
+ if msg.RelatesTo.Type == event.RelReplace && msg.NewContent != nil {
+ // Is it an edit?
+ rmsg.ID = string(msg.RelatesTo.EventID)
+ rmsg.Text = msg.NewContent.Body
+ } else if msg.RelatesTo.Type == event.RelReference && msg.RelatesTo.InReplyTo != nil {
+ // Is it a reply?
+ body := msg.Body
+ if !b.GetBool("keepquotedreply") {
+ for strings.HasPrefix(body, "> ") {
+ lineIdx := strings.Index(body, "\n\n")
+ if lineIdx == -1 {
+ break
+ }
+
+ body = body[(lineIdx + 2):]
+ }
+ }
+
+ rmsg.ParentID = string(msg.RelatesTo.EventID)
+ rmsg.Text = body
+ }
+ }
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
+ b.Remote <- rmsg
+}
+
+// handleDownloadFile handles file download
+func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, msg event.MessageEventContent) error {
+ rmsg.Extra = make(map[string][]interface{})
+ if msg.URL == "" || msg.Info == nil {
+ b.Log.Error("couldn't download a file with no URL or no file informations (invalid event ?)")
+ b.Log.Debugf("Full Message content:\n%#v", msg)
+ }
+
+ url := strings.ReplaceAll(string(msg.URL), "mxc://", b.GetString("Server")+"/_matrix/media/v1/download/")
+ filename := msg.Body
+
+ // check if we have an image uploaded without extension
+ if !strings.Contains(filename, ".") {
+ mext, _ := mime.ExtensionsByType(msg.Info.MimeType)
+ if len(mext) > 0 {
+ filename += mext[0]
+ } else if msg.MsgType == event.MsgImage {
+ // just a default .png extension if we don't have mime info
+ filename += ".png"
+ }
+ }
+
+ // check if the size is ok
+ err := helper.HandleDownloadSize(b.Log, rmsg, filename, int64(msg.Info.Size), b.General)
+ if err != nil {
+ return err
+ }
+ // actually download the file
+ data, err := helper.DownloadFile(url)
+ if err != nil {
+ return fmt.Errorf("download %s failed %#v", url, err)
+ }
+ // add the downloaded data to the message
+ helper.HandleDownloadData(b.Log, rmsg, filename, "", url, data, b.General)
+ return nil
+}
+
+// handleUploadFiles handles native upload of files.
+func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel id.RoomID) (string, error) {
+ for _, f := range msg.Extra["file"] {
+ if fi, ok := f.(config.FileInfo); ok {
+ b.handleUploadFile(msg, channel, &fi)
+ }
+ }
+ return "", nil
+}
+
+// handleUploadFile handles native upload of a file.
+//nolint: funlen
+func (b *Bmatrix) handleUploadFile(msg *config.Message, channel id.RoomID, fi *config.FileInfo) {
+ content := bytes.NewReader(*fi.Data)
+ sp := strings.Split(fi.Name, ".")
+ mtype := mime.TypeByExtension("." + sp[len(sp)-1])
+
+ // image and video uploads send no username, we have to do this ourself here #715
+ //nolint:exhaustruct
+ m := event.MessageEventContent{
+ MsgType: event.MsgText,
+ Body: fi.Comment,
+ FormattedBody: fi.Comment,
+ }
+
+ _, err := b.sendMessageEventWithRetries(channel, m, msg.Username)
+ if err != nil {
+ b.Log.Errorf("file comment failed: %#v", err)
+ }
+
+ b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
+
+ var res *matrix.RespMediaUpload
+ //nolint:exhaustruct
+ req := matrix.ReqUploadMedia{
+ Content: content,
+ ContentType: mtype,
+ ContentLength: fi.Size,
+ }
+
+ err = b.retry(func() error {
+ res, err = b.mc.UploadMedia(req)
+
+ return err
+ })
+
+ if err != nil {
+ b.Log.Errorf("file upload failed: %#v", err)
+ return
+ }
+
+ b.Log.Debugf("result: %#v", res)
+
+ //nolint:exhaustruct
+ m = event.MessageEventContent{
+ Body: fi.Name,
+ URL: res.ContentURI.CUString(),
+ }
+
+ switch {
+ case strings.Contains(mtype, "video"):
+ b.Log.Debugf("sendVideo %s", res.ContentURI)
+
+ m.MsgType = event.MsgVideo
+ case strings.Contains(mtype, "image"):
+ b.Log.Debugf("sendImage %s", res.ContentURI)
+
+ m.MsgType = event.MsgImage
+ case strings.Contains(mtype, "audio"):
+ b.Log.Debugf("sendAudio %s", res.ContentURI)
+
+ m.MsgType = event.MsgAudio
+ //nolint:exhaustruct
+ m.Info = &event.FileInfo{
+ MimeType: mtype,
+ Size: len(*fi.Data),
+ }
+ default:
+ b.Log.Debugf("sendFile %s", res.ContentURI)
+
+ m.MsgType = event.MsgFile
+ //nolint:exhaustruct
+ m.Info = &event.FileInfo{
+ MimeType: mtype,
+ Size: len(*fi.Data),
+ }
+ }
+
+ _, err = b.sendMessageEventWithRetries(channel, m, msg.Username)
+ if err != nil {
+ b.Log.Errorf("sending the message referencing the uploaded file failed: %#v", err)
+ }
+}