428 lines
12 KiB
Go
428 lines
12 KiB
Go
package bmatrix
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"mime"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
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"
|
|
)
|
|
|
|
func (b *Bmatrix) handlematrix() {
|
|
syncer, ok := b.mc.Syncer.(*matrix.DefaultSyncer)
|
|
if !ok {
|
|
b.Log.Errorf("couldn't convert the Syncer object to a DefaultSyncer structure, the matrix bridge won't work")
|
|
|
|
return
|
|
}
|
|
|
|
// register our custom filtering function
|
|
syncer.OnSync(b.DontProcessOldEvents)
|
|
|
|
eventsTypes := []event.Type{event.EventRedaction, event.EventMessage, event.StateMember, event.EventSticker, event.EphemeralEventReceipt}
|
|
if b.GetBool("ShowUserTyping") {
|
|
eventsTypes = append(eventsTypes, event.EphemeralEventTyping)
|
|
}
|
|
for _, evType := range eventsTypes {
|
|
syncer.OnEventType(evType, func(source matrix.EventSource, ev *event.Event) {
|
|
b.handleEvent(originClassicSyncer, ev)
|
|
})
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-b.stopNormalSync:
|
|
b.stopNormalSyncAck <- struct{}{}
|
|
|
|
return
|
|
default:
|
|
|
|
if err := b.mc.Sync(); err != nil {
|
|
b.Log.Warningf("Sync() returned %#v", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// 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 ev.Type == event.EphemeralEventReceipt {
|
|
// we do not support read receipts across servers, considering that
|
|
// multiple services (e.g. Discord) don't expose that information
|
|
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
|
|
}
|
|
|
|
// This needs to be defined before rejecting bridging events, as we rely on this to cache
|
|
// avatar URLs sent with appService (otherwise we would upload one avatar per message sent
|
|
// across the bridge!).
|
|
// As such, beware! Moving this below the b.ignoreBridgingEvents condiiton would appear to
|
|
// work, but it would also lead to a high file upload rate, until being eventually
|
|
// rate-limited by the homeserver
|
|
if ev.Type == event.StateMember {
|
|
b.handleMemberChange(ev)
|
|
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
if b.ignoreBridgingEvents(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 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
|
|
}
|
|
|
|
b.Log.Debugf("== Receiving event: %#v (appService=%t)", ev, origin == originAppService)
|
|
|
|
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)
|
|
b.cacheAvatarURL(ev.RoomID, ev.Sender, member.AvatarURL)
|
|
} else if member.Membership == event.MembershipLeave || member.Membership == event.MembershipBan {
|
|
b.UserCache.removeFromCache(ev.RoomID, 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
|
|
|
|
rmsg.Avatar = b.getAvatarURL(ev.RoomID, ev.Sender)
|
|
|
|
if ev.Type == event.EventSticker {
|
|
err := b.handleSticker(&rmsg, ev, *msg)
|
|
if err != nil {
|
|
b.Log.Errorf("sticker handling failed: %#v", err)
|
|
}
|
|
}
|
|
|
|
//nolint:exhaustive
|
|
switch msg.MsgType {
|
|
case event.MsgEmote:
|
|
// Do we have a /me action
|
|
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)
|
|
}
|
|
case event.MsgNotice:
|
|
// Support for IRC NOTICE commands/[matrix] m.notice
|
|
rmsg.Event = config.EventNotice
|
|
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
|
|
}
|
|
|
|
func (b *Bmatrix) handleSticker(rmsg *config.Message, ev *event.Event, msg event.MessageEventContent) error {
|
|
if msg.URL == "" || msg.Info == nil {
|
|
b.Log.Error("couldn't download a sticker with no URL or no file informations (invalid event ?)")
|
|
b.Log.Debugf("Full Message content:\n%#v", msg)
|
|
}
|
|
|
|
mext, _ := mime.ExtensionsByType(msg.Info.MimeType)
|
|
filename := fmt.Sprintf("sticker-%d.%s", time.Now().UnixNano(), mext)
|
|
|
|
// 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
|
|
url := b.mc.GetDownloadURL(msg.URL.ParseOrIgnore())
|
|
data, err := helper.DownloadFile(url)
|
|
if err != nil {
|
|
return fmt.Errorf("download %s failed %#v", url, err)
|
|
}
|
|
|
|
rmsg.Extra = make(map[string][]interface{})
|
|
|
|
// we have a terrible alt-text support, so right now we emit the information but nobody uses it
|
|
rmsg.Extra["Alt-Text"] = []interface{}{msg.Body}
|
|
|
|
// add the downloaded data to the message
|
|
helper.HandleDownloadData2(b.Log, rmsg, filename, string(ev.ID), "", url, data, b.General)
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 := b.mc.GetDownloadURL(msg.URL.ParseOrIgnore())
|
|
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, msg.Avatar)
|
|
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, msg.Avatar)
|
|
if err != nil {
|
|
b.Log.Errorf("sending the message referencing the uploaded file failed: %#v", err)
|
|
}
|
|
}
|