diff options
Diffstat (limited to 'bridge/matrix/handlers.go')
-rw-r--r-- | bridge/matrix/handlers.go | 344 |
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) + } +} |