From 4393772ccc4ef8f54c4fced764b9e11bdda00155 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 27 May 2023 13:01:24 +0300 Subject: [PATCH] Add options to improve handling of webhook messages sent by other bridges --- config/bridge.go | 11 +++++++++-- config/upgrade.go | 2 ++ example-config.yaml | 6 ++++++ portal.go | 3 +++ portal_convert.go | 30 ++++++++++++++++++++++++++++++ puppet.go | 10 ++++++++-- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index dd1f080..9118317 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -51,6 +51,8 @@ type BridgeConfig struct { DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"` DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"` FederateRooms bool `yaml:"federate_rooms"` + PrefixWebhookMessages bool `yaml:"prefix_webhook_messages"` + EnableWebhookAvatars bool `yaml:"enable_webhook_avatars"` UseDiscordCDNUpload bool `yaml:"use_discord_cdn_upload"` CacheMedia string `yaml:"cache_media"` @@ -287,9 +289,14 @@ func (bc BridgeConfig) FormatUsername(userID string) string { return buffer.String() } -func (bc BridgeConfig) FormatDisplayname(user *discordgo.User) string { +type DisplaynameParams struct { + *discordgo.User + Webhook bool +} + +func (bc BridgeConfig) FormatDisplayname(user *discordgo.User, webhook bool) string { var buffer strings.Builder - _ = bc.displaynameTemplate.Execute(&buffer, user) + _ = bc.displaynameTemplate.Execute(&buffer, &DisplaynameParams{user, webhook}) return buffer.String() } diff --git a/config/upgrade.go b/config/upgrade.go index e207c41..71175aa 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -55,6 +55,8 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete") helper.Copy(up.Bool, "bridge", "delete_guild_on_leave") helper.Copy(up.Bool, "bridge", "federate_rooms") + helper.Copy(up.Bool, "bridge", "prefix_webhook_messages") + helper.Copy(up.Bool, "bridge", "enable_webhook_avatars") helper.Copy(up.Bool, "bridge", "use_discord_cdn_upload") helper.Copy(up.Bool, "bridge", "media_patterns", "enabled") helper.Copy(up.Str, "bridge", "cache_media") diff --git a/example-config.yaml b/example-config.yaml index f45aabd..63ded47 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -85,6 +85,7 @@ bridge: # .Discriminator - The 4 numbers after the name on Discord # .Bot - Whether the user is a bot # .System - Whether the user is an official system user + # .Webhook - Whether the user is a webhook displayname_template: '{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}' # Displayname template for Discord channels (bridged as rooms, or spaces when type=4). # Available variables: @@ -146,6 +147,11 @@ bridge: # Whether or not created rooms should have federation enabled. # If false, created portal rooms will never be federated. federate_rooms: true + # Prefix messages from webhooks with the profile info? This can be used along with a custom displayname_template + # to better handle webhooks that change their name all the time (like ones used by bridges). + prefix_webhook_messages: false + # Bridge webhook avatars? + enable_webhook_avatars: true # Should the bridge upload media to the Discord CDN directly before sending the message when using a user token, # like the official client does? The other option is sending the media in the message send request as a form part # (which is always used by bots and webhooks). diff --git a/portal.go b/portal.go index 533b700..3ddba24 100644 --- a/portal.go +++ b/portal.go @@ -897,6 +897,9 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess Msg("Dropping non-text edit") return } + if msg.WebhookID != "" { + addWebhookMeta(converted, msg) + } converted.Content.Mentions = portal.convertDiscordMentions(msg, "", false) converted.Content.SetEdit(existing[0].MXID) // Never actually mention new users of edits, only include mentions inside m.new_content diff --git a/portal_convert.go b/portal_convert.go index 8ebf09e..2a8fb55 100644 --- a/portal_convert.go +++ b/portal_convert.go @@ -308,9 +308,33 @@ func (portal *Portal) convertDiscordMessage(ctx context.Context, intent *appserv parts = append(parts, part) } } + if msg.WebhookID != "" { + for _, part := range parts { + addWebhookMeta(part, msg) + } + } return parts } +func addWebhookMeta(part *ConvertedMessage, msg *discordgo.Message) { + if msg.WebhookID == "" { + return + } + if part.Extra == nil { + part.Extra = make(map[string]any) + } + part.Extra["fi.mau.discord.webhook_metadata"] = map[string]any{ + "id": msg.WebhookID, + "name": msg.Author.Username, + "avatar_id": msg.Author.Avatar, + "avatar_url": msg.Author.AvatarURL(""), + } + part.Extra["com.beeper.per_message_profile"] = map[string]any{ + "avatar_url": msg.Author.AvatarURL(""), + "displayname": msg.Author.Username, + } +} + const ( embedHTMLWrapper = `
%s
` embedHTMLWrapperColor = `
%s
` @@ -628,5 +652,11 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app "com.beeper.linkpreviews": previews, } + if msg.WebhookID != "" && portal.bridge.Config.Bridge.PrefixWebhookMessages { + content.EnsureHasHTML() + content.Body = fmt.Sprintf("%s: %s", msg.Author.Username, content.Body) + content.FormattedBody = fmt.Sprintf("%s: %s", html.EscapeString(msg.Author.Username), content.FormattedBody) + } + return &ConvertedMessage{Type: event.EventMessage, Content: &content, Extra: extraContent} } diff --git a/puppet.go b/puppet.go index d3246aa..44406e8 100644 --- a/puppet.go +++ b/puppet.go @@ -207,7 +207,7 @@ func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) { } func (puppet *Puppet) UpdateName(info *discordgo.User) bool { - newName := puppet.bridge.Config.Bridge.FormatDisplayname(info) + newName := puppet.bridge.Config.Bridge.FormatDisplayname(info, puppet.IsWebhook) if puppet.Name == newName && puppet.NameSet { return false } @@ -229,6 +229,9 @@ func (puppet *Puppet) UpdateName(info *discordgo.User) bool { } func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool { + if puppet.IsWebhook && !puppet.bridge.Config.Bridge.EnableWebhookAvatars { + info.Avatar = "" + } if puppet.Avatar == info.Avatar && puppet.AvatarSet { return false } @@ -324,7 +327,7 @@ func (puppet *Puppet) UpdateContactInfo(info *discordgo.User) bool { puppet.IsBot = info.Bot changed = true } - if changed { + if (changed && !puppet.IsWebhook) || !puppet.ContactInfoSet { puppet.ContactInfoSet = false puppet.ResendContactInfo() return true @@ -345,6 +348,9 @@ func (puppet *Puppet) ResendContactInfo() { "com.beeper.bridge.network": puppet.bridge.BeeperNetworkName, "com.beeper.bridge.is_network_bot": puppet.IsBot, } + if puppet.IsWebhook { + contactInfo["com.beeper.bridge.identifiers"] = []string{} + } err := puppet.DefaultIntent().BeeperUpdateProfile(contactInfo) if err != nil { puppet.log.Warn().Err(err).Msg("Failed to store custom contact info in profile")