diff --git a/commands.go b/commands.go index 2d3aa7e..7ee1c21 100644 --- a/commands.go +++ b/commands.go @@ -55,6 +55,7 @@ func (br *DiscordBridge) RegisterCommands() { cmdLogout, cmdReconnect, cmdDisconnect, + cmdSetRelay, cmdGuilds, cmdRejoinSpace, cmdDeleteAllPortals, @@ -352,6 +353,91 @@ func fnRejoinSpace(ce *WrappedCommandEvent) { } } +var cmdSetRelay = &commands.FullHandler{ + Func: wrapCommand(fnSetRelay), + Name: "set-relay", + Help: commands.HelpMeta{ + Section: commands.HelpSectionUnclassified, + Description: "Create or set a relay webhook for a portal", + Args: "[room ID] <​--url URL> OR <​--create [name]>", + }, + RequiresLogin: true, +} + +const webhookURLFormat = "https://discord.com/api/webhooks/%d/%s" + +const selectRelayHelp = "Usage: `$cmdprefix [room ID] <​--url URL> OR <​--create [name]>`" + +func fnSetRelay(ce *WrappedCommandEvent) { + portal := ce.Portal + if len(ce.Args) > 0 && strings.HasPrefix(ce.Args[0], "!") { + portal = ce.Bridge.GetPortalByMXID(id.RoomID(ce.Args[0])) + if portal == nil { + ce.Reply("Portal with room ID %s not found", ce.Args[0]) + return + } + ce.Args = ce.Args[1:] + } else if portal == nil { + ce.Reply("You must either run the command in a portal, or specify an internal room ID as the first parameter") + return + } + if len(ce.Args) == 0 { + ce.Reply(selectRelayHelp) + return + } + log := ce.ZLog.With().Str("channel_id", portal.Key.ChannelID).Logger() + createType := strings.ToLower(strings.TrimLeft(ce.Args[0], "-")) + var webhookID int64 + var webhookSecret string + switch createType { + case "url": + if len(ce.Args) < 2 { + ce.Reply("Usage: `$cmdprefix [room ID] --url ") + return + } + ce.Redact() + _, err := fmt.Sscanf(ce.Args[1], webhookURLFormat, &webhookID, &webhookSecret) + if err != nil { + log.Warn().Str("webhook_url", ce.Args[1]).Err(err).Msg("Failed to parse provided webhook URL") + ce.Reply("Invalid webhook URL") + return + } + case "create": + perms, err := ce.User.Session.UserChannelPermissions(ce.User.DiscordID, portal.Key.ChannelID) + if err != nil { + log.Warn().Err(err).Msg("Failed to check user permissions") + ce.Reply("Failed to check if you have permission to create webhooks") + return + } else if perms&discordgo.PermissionManageWebhooks == 0 { + log.Debug().Int64("perms", perms).Msg("User doesn't have permissions to manage webhooks in channel") + ce.Reply("You don't have permission to manage webhooks in that channel") + return + } + name := "mautrix" + if len(ce.Args) > 1 { + name = strings.Join(ce.Args[1:], " ") + } + log.Debug().Str("webhook_name", name).Msg("Creating webhook") + webhook, err := ce.User.Session.WebhookCreate(portal.Key.ChannelID, name, "") + if err != nil { + log.Warn().Err(err).Msg("Failed to create webhook") + ce.Reply("Failed to create webhook: %v", err) + return + } + webhookID, _ = strconv.ParseInt(webhook.ID, 10, 64) + ce.Reply("Created webhook %s", webhook.Name) + webhookSecret = webhook.Token + default: + ce.Reply(selectRelayHelp) + return + } + log.Debug().Int64("webhook_id", webhookID).Msg("Setting portal relay webhook") + portal.RelayWebhookID = strconv.FormatInt(webhookID, 10) + portal.RelayWebhookSecret = webhookSecret + portal.Update() + ce.Reply("Saved webhook ID %s as portal relay webhook", portal.RelayWebhookID) +} + var cmdGuilds = &commands.FullHandler{ Func: wrapCommand(fnGuilds), Name: "guilds", diff --git a/config/upgrade.go b/config/upgrade.go index 173d328..d0c5e43 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -25,6 +25,8 @@ import ( func DoUpgrade(helper *up.Helper) { bridgeconfig.Upgrader.DoUpgrade(helper) + helper.Copy(up.Str|up.Null, "homeserver", "public_address") + helper.Copy(up.Str, "bridge", "username_template") helper.Copy(up.Str, "bridge", "displayname_template") helper.Copy(up.Str, "bridge", "channel_name_template") diff --git a/database/portal.go b/database/portal.go index 680934c..ba6bccd 100644 --- a/database/portal.go +++ b/database/portal.go @@ -16,7 +16,7 @@ const ( portalSelect = ` SELECT dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid, plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, - encrypted, in_space, first_event_id + encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret FROM portal ` ) @@ -121,16 +121,19 @@ type Portal struct { InSpace id.RoomID FirstEventID id.EventID + + RelayWebhookID string + RelayWebhookSecret string } func (p *Portal) Scan(row dbutil.Scannable) *Portal { - var otherUserID, guildID, parentID, mxid, firstEventID sql.NullString + var otherUserID, guildID, parentID, mxid, firstEventID, relayWebhookID, relayWebhookSecret sql.NullString var chanType int32 var avatarURL string err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &chanType, &otherUserID, &guildID, &parentID, &mxid, &p.PlainName, &p.Name, &p.NameSet, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet, - &p.Encrypted, &p.InSpace, &firstEventID) + &p.Encrypted, &p.InSpace, &firstEventID, &relayWebhookID, &relayWebhookSecret) if err != nil { if err != sql.ErrNoRows { @@ -148,6 +151,8 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal { p.Type = discordgo.ChannelType(chanType) p.FirstEventID = id.EventID(firstEventID.String) p.AvatarURL, _ = id.ParseContentURI(avatarURL) + p.RelayWebhookID = relayWebhookID.String + p.RelayWebhookSecret = relayWebhookSecret.String return p } @@ -156,13 +161,13 @@ func (p *Portal) Insert() { query := ` INSERT INTO portal (dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid, plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, - encrypted, in_space, first_event_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) ` _, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)), p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, - p.Encrypted, p.InSpace, p.FirstEventID.String()) + p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret)) if err != nil { p.log.Warnfln("Failed to insert %s: %v", p.Key, err) @@ -175,13 +180,13 @@ func (p *Portal) Update() { UPDATE portal SET type=$1, other_user_id=$2, dc_guild_id=$3, dc_parent_id=$4, mxid=$5, plain_name=$6, name=$7, name_set=$8, topic=$9, topic_set=$10, avatar=$11, avatar_url=$12, avatar_set=$13, - encrypted=$14, in_space=$15, first_event_id=$16 - WHERE dcid=$17 AND receiver=$18 + encrypted=$14, in_space=$15, first_event_id=$16, relay_webhook_id=$17, relay_webhook_secret=$18 + WHERE dcid=$19 AND receiver=$20 ` _, err := p.db.Exec(query, p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)), p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, - p.Encrypted, p.InSpace, p.FirstEventID.String(), + p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret), p.Key.ChannelID, p.Key.Receiver) if err != nil { diff --git a/database/upgrades/00-latest-revision.sql b/database/upgrades/00-latest-revision.sql index 9e0d5eb..dbfe269 100644 --- a/database/upgrades/00-latest-revision.sql +++ b/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v14: Latest revision +-- v0 -> v15: Latest revision CREATE TABLE guild ( dcid TEXT PRIMARY KEY, @@ -39,6 +39,9 @@ CREATE TABLE portal ( first_event_id TEXT NOT NULL, + relay_webhook_id TEXT, + relay_webhook_secret TEXT, + PRIMARY KEY (dcid, receiver), CONSTRAINT portal_parent_fkey FOREIGN KEY (dc_parent_id, dc_parent_receiver) REFERENCES portal (dcid, receiver) ON DELETE CASCADE, CONSTRAINT portal_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild(dcid) ON DELETE CASCADE diff --git a/database/upgrades/15-portal-relay-webhook.sql b/database/upgrades/15-portal-relay-webhook.sql new file mode 100644 index 0000000..0035d00 --- /dev/null +++ b/database/upgrades/15-portal-relay-webhook.sql @@ -0,0 +1,3 @@ +-- v15: Store relay webhook URL for portals +ALTER TABLE portal ADD COLUMN relay_webhook_id TEXT; +ALTER TABLE portal ADD COLUMN relay_webhook_secret TEXT; diff --git a/example-config.yaml b/example-config.yaml index 01a7147..e3f985a 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -2,6 +2,9 @@ homeserver: # The address that this appservice can use to connect to the homeserver. address: https://matrix.example.com + # Publicly accessible base URL for media, used for avatars in relay mode. + # If not set, the connection address above will be used. + public_address: null # The domain of the homeserver (also known as server_name, used for MXIDs, etc). domain: example.com diff --git a/formatter.go b/formatter.go index 0b99ba3..4255a57 100644 --- a/formatter.go +++ b/formatter.go @@ -21,6 +21,7 @@ import ( "regexp" "strings" + "github.com/bwmarrin/discordgo" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" @@ -91,6 +92,16 @@ func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLink } const formatterContextPortalKey = "fi.mau.discord.portal" +const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions" + +func appendIfNotContains(arr []string, newItem string) []string { + for _, item := range arr { + if item == newItem { + return arr + } + } + return append(arr, newItem) +} func (br *DiscordBridge) pillConverter(displayname, mxid, eventID string, ctx format.Context) string { if len(mxid) == 0 { @@ -124,12 +135,15 @@ func (br *DiscordBridge) pillConverter(displayname, mxid, eventID string, ctx fo } } } else if mxid[0] == '@' { + mentions := ctx.ReturnData[formatterContextAllowedMentionsKey].(*discordgo.MessageAllowedMentions) parsedID, ok := br.ParsePuppetMXID(id.UserID(mxid)) if ok { + mentions.Users = appendIfNotContains(mentions.Users, parsedID) return fmt.Sprintf("<@%s>", parsedID) } mentionedUser := br.GetUserByMXID(id.UserID(mxid)) if mentionedUser != nil && mentionedUser.DiscordID != "" { + mentions.Users = appendIfNotContains(mentions.Users, mentionedUser.DiscordID) return fmt.Sprintf("<@%s>", mentionedUser.DiscordID) } } @@ -195,12 +209,18 @@ var matrixHTMLParser = &format.HTMLParser{ }, } -func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) string { +func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) (string, *discordgo.MessageAllowedMentions) { + allowedMentions := &discordgo.MessageAllowedMentions{ + Parse: []discordgo.AllowedMentionType{}, + Users: []string{}, + RepliedUser: true, + } if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 { ctx := format.NewContext() ctx.ReturnData[formatterContextPortalKey] = portal - return variationselector.FullyQualify(matrixHTMLParser.Parse(content.FormattedBody, ctx)) + ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions + return variationselector.FullyQualify(matrixHTMLParser.Parse(content.FormattedBody, ctx)), allowedMentions } else { - return variationselector.FullyQualify(escapeDiscordMarkdown(content.Body)) + return variationselector.FullyQualify(escapeDiscordMarkdown(content.Body)), allowedMentions } } diff --git a/go.mod b/go.mod index 4f7f24c..98744e8 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/yuin/goldmark v1.5.4 maunium.net/go/maulogger/v2 v2.4.1 - maunium.net/go/mautrix v0.15.0-beta.1.0.20230226232632-00f40652f33d + maunium.net/go/mautrix v0.15.0-beta.1.0.20230227211640-c8b3566fb7ba ) require ( @@ -37,4 +37,4 @@ require ( maunium.net/go/mauflag v1.0.0 // indirect ) -replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230226184350-ef6bcfe94f07 +replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230227224009-daaee0136f88 diff --git a/go.sum b/go.sum index b3b049e..e6a6d9f 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/beeper/discordgo v0.0.0-20230226184350-ef6bcfe94f07 h1:YajAt8iJkBn4aavUuftybeXUaeN4p0DPCE3a4wxE2Oc= -github.com/beeper/discordgo v0.0.0-20230226184350-ef6bcfe94f07/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/beeper/discordgo v0.0.0-20230227224009-daaee0136f88 h1:sZUZP+ClkQk1uC0KB6dYJ+v6Ygao3RaPKp/3leRjYik= +github.com/beeper/discordgo v0.0.0-20230227224009-daaee0136f88/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -82,5 +82,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.15.0-beta.1.0.20230226232632-00f40652f33d h1:16Q4co5TusYEovGLiHSkT6FY6fFn5tNNLCR3FvGCLFk= -maunium.net/go/mautrix v0.15.0-beta.1.0.20230226232632-00f40652f33d/go.mod h1:AE3TCX9q4W7fYfrL/1RsuOell9rTUBO27XUULuwArH4= +maunium.net/go/mautrix v0.15.0-beta.1.0.20230227211640-c8b3566fb7ba h1:OS+zjLTyeqxzMcgnbBbXlZSr0B2yfalCo2lNhC2wP5A= +maunium.net/go/mautrix v0.15.0-beta.1.0.20230227211640-c8b3566fb7ba/go.mod h1:AE3TCX9q4W7fYfrL/1RsuOell9rTUBO27XUULuwArH4= diff --git a/portal.go b/portal.go index 3484d6f..f5a8083 100644 --- a/portal.go +++ b/portal.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "errors" "fmt" "reflect" @@ -41,6 +42,8 @@ type portalMatrixMessage struct { user *User } +var relayClient, _ = discordgo.New("") + type Portal struct { *database.Portal @@ -85,7 +88,7 @@ func (portal *Portal) MarkEncrypted() { } func (portal *Portal) ReceiveMatrixEvent(user bridge.User, evt *event.Event) { - if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser /*|| portal.HasRelaybot()*/ { + if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser || portal.RelayWebhookID != "" { portal.matrixMessages <- portalMatrixMessage{user: user.(*User), evt: evt} } } @@ -971,6 +974,7 @@ var ( errUnknownRelationType = errors.New("unknown relation type") errTargetNotFound = errors.New("target event not found") errUnknownEmoji = errors.New("unknown emoji") + errCantStartThread = errors.New("can't create thread without being logged into Discord") ) func errorToStatusReason(err error) (reason event.MessageStatusReason, status event.MessageStatus, isCertain, sendNotice bool, humanMessage string) { @@ -981,7 +985,8 @@ func errorToStatusReason(err error) (reason event.MessageStatusReason, status ev errors.Is(err, errUnknownEmoji), errors.Is(err, id.InvalidContentURI), errors.Is(err, attachment.UnsupportedVersion), - errors.Is(err, attachment.UnsupportedAlgorithm): + errors.Is(err, attachment.UnsupportedAlgorithm), + errors.Is(err, errCantStartThread): return event.MessageStatusUnsupported, event.MessageStatusFail, true, true, "" case errors.Is(err, attachment.HashMismatch), errors.Is(err, attachment.InvalidKey), @@ -1065,6 +1070,22 @@ func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part strin } } +func (portal *Portal) getRelayUserMeta(sender *User) (name, avatarURL string) { + member := portal.bridge.StateStore.GetMember(portal.MXID, sender.MXID) + name = member.Displayname + if name == "" { + name = sender.MXID.String() + } + mxc := member.AvatarURL.ParseOrIgnore() + if !mxc.IsEmpty() { + avatarURL = mautrix.BuildURL( + portal.bridge.PublicHSAddress, + "_matrix", "media", "v3", "download", mxc.Homeserver, mxc.FileID, + ).String() + } + return +} + func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver { go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring") @@ -1078,14 +1099,23 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { } channelID := portal.Key.ChannelID + sess := sender.Session var threadID string if editMXID := content.GetRelatesTo().GetReplaceID(); editMXID != "" && content.NewContent != nil { edits := portal.bridge.DB.Message.GetByMXID(portal.Key, editMXID) if edits != nil { - discordContent := portal.parseMatrixHTML(content.NewContent) - // TODO save edit in message table - _, err := sender.Session.ChannelMessageEdit(edits.DiscordProtoChannelID(), edits.DiscordID, discordContent) + discordContent, allowedMentions := portal.parseMatrixHTML(content.NewContent) + var err error + if sess != nil { + // TODO save edit in message table + _, err = sess.ChannelMessageEdit(edits.DiscordProtoChannelID(), edits.DiscordID, discordContent) + } else { + _, err = relayClient.WebhookMessageEdit(portal.RelayWebhookID, portal.RelayWebhookSecret, edits.DiscordID, &discordgo.WebhookEdit{ + Content: &discordContent, + AllowedMentions: allowedMentions, + }) + } go portal.sendMessageMetrics(evt, err, "Failed to edit") } else { go portal.sendMessageMetrics(evt, fmt.Errorf("%w %s", errUnknownEditTarget, editMXID), "Ignoring") @@ -1096,6 +1126,11 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { if existingThread != nil { threadID = existingThread.ID } else { + if sess == nil { + // TODO start thread with bot? + go portal.sendMessageMetrics(evt, errCantStartThread, "Dropping") + return + } var err error threadID, err = portal.startThreadFromMatrix(sender, threadRoot) if err != nil { @@ -1129,48 +1164,85 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { } } } - sendReq.Content = portal.parseMatrixHTML(content) + sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content) case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo: data, err := downloadMatrixAttachment(portal.MainIntent(), content) if err != nil { go portal.sendMessageMetrics(evt, err, "Error downloading media in") return } - - att := &discordgo.MessageAttachment{ - ID: "0", - Filename: content.Body, - Description: description, - } - sendReq.Attachments = []*discordgo.MessageAttachment{att} + filename := content.Body if content.FileName != "" && content.FileName != content.Body { - att.Filename = content.FileName - sendReq.Content = portal.parseMatrixHTML(content) + filename = content.FileName + sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content) } - prep, err := sender.Session.ChannelAttachmentCreate(channelID, &discordgo.ReqPrepareAttachments{ - Files: []*discordgo.FilePrepare{{ - Size: len(data), - Name: att.Filename, - ID: sender.NextDiscordUploadID(), - }}, - }) - if err != nil { - go portal.sendMessageMetrics(evt, err, "Error preparing to reupload media in") - return - } - prepared := prep.Attachments[0] - att.UploadedFilename = prepared.UploadFilename - err = uploadDiscordAttachment(prepared.UploadURL, data) - if err != nil { - go portal.sendMessageMetrics(evt, err, "Error reuploading media in") - return + + if sess != nil && sess.IsUser { + att := &discordgo.MessageAttachment{ + ID: "0", + Filename: filename, + Description: description, + } + sendReq.Attachments = []*discordgo.MessageAttachment{att} + prep, err := sender.Session.ChannelAttachmentCreate(channelID, &discordgo.ReqPrepareAttachments{ + Files: []*discordgo.FilePrepare{{ + Size: len(data), + Name: att.Filename, + ID: sender.NextDiscordUploadID(), + }}, + }) + if err != nil { + go portal.sendMessageMetrics(evt, err, "Error preparing to reupload media in") + return + } + prepared := prep.Attachments[0] + att.UploadedFilename = prepared.UploadFilename + err = uploadDiscordAttachment(prepared.UploadURL, data) + if err != nil { + go portal.sendMessageMetrics(evt, err, "Error reuploading media in") + return + } + } else { + sendReq.Files = []*discordgo.File{{ + Name: filename, + ContentType: content.Info.MimeType, + Reader: bytes.NewReader(data), + }} } default: go portal.sendMessageMetrics(evt, fmt.Errorf("%w %q", errUnknownMsgType, content.MsgType), "Ignoring") return } + if sess != nil { + // AllowedMentions must not be set for real users, and it's also not that useful for personal bots. + // It's only important for relaying, where the webhook may have higher permissions than the user on Matrix. + sendReq.AllowedMentions = nil + } else if strings.Contains(sendReq.Content, "@everyone") || strings.Contains(sendReq.Content, "@here") { + powerLevels, err := portal.MainIntent().PowerLevels(portal.MXID) + if err != nil { + portal.log.Warnfln("Failed to get power levels in %s to check if %s can @everyone: %v", portal.MXID, sender.MXID, err) + } else if powerLevels.GetUserLevel(sender.MXID) >= powerLevels.Notifications.Room() { + sendReq.AllowedMentions.Parse = append(sendReq.AllowedMentions.Parse, discordgo.AllowedMentionTypeEveryone) + } + } sendReq.Nonce = generateNonce() - msg, err := sender.Session.ChannelMessageSendComplex(channelID, &sendReq) + var msg *discordgo.Message + var err error + if sess != nil { + msg, err = sess.ChannelMessageSendComplex(channelID, &sendReq) + } else { + username, avatarURL := portal.getRelayUserMeta(sender) + msg, err = relayClient.WebhookThreadExecute(portal.RelayWebhookID, portal.RelayWebhookSecret, true, threadID, &discordgo.WebhookParams{ + Content: sendReq.Content, + Username: username, + AvatarURL: avatarURL, + TTS: sendReq.TTS, + Files: sendReq.Files, + Components: sendReq.Components, + Embeds: sendReq.Embeds, + AllowedMentions: sendReq.AllowedMentions, + }) + } go portal.sendMessageMetrics(evt, err, "Error sending") if msg != nil { dbMsg := portal.bridge.DB.Message.New() @@ -1180,7 +1252,11 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { dbMsg.AttachmentID = msg.Attachments[0].ID } dbMsg.MXID = evt.ID - dbMsg.SenderID = sender.DiscordID + if sess != nil { + dbMsg.SenderID = sender.DiscordID + } else { + dbMsg.SenderID = portal.RelayWebhookID + } dbMsg.Timestamp, _ = discordgo.SnowflakeTimestamp(msg.ID) dbMsg.ThreadID = threadID dbMsg.Insert() @@ -1333,6 +1409,9 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) { if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver { go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring") return + } else if !sender.IsLoggedIn() { + //go portal.sendMessageMetrics(evt, errReactionUserNotLoggedIn, "Ignoring") + return } reaction := evt.Content.AsReaction()