24 Commits

Author SHA1 Message Date
Tulir Asokan
00465bb715 Bump version to v0.5.0 2023-06-16 14:42:12 +03:00
Tulir Asokan
cf640ac83d Ignore incoming typing notifications from logged-in users 2023-06-14 10:56:24 +03:00
Tulir Asokan
67c8d9237e Remove updating custom ghost info on startup 2023-06-09 17:28:51 +03:00
Tulir Asokan
b2d7077e8d Update mautrix-go to enable appservice websockets 2023-06-09 17:28:24 +03:00
Tulir Asokan
d5db336eee Update mautrix-go 2023-06-09 12:41:17 +03:00
Tulir Asokan
b153e70f2a Don't send missed message warning on initial backfill 2023-06-06 17:03:33 +03:00
Tulir Asokan
cc30353075 Update mautrix-go 2023-06-06 13:52:43 +03:00
Tulir Asokan
4c62fe8b12 Don't add reply sender to mentions array manually
Discord already adds it to the native mentions array
2023-06-04 11:34:04 +03:00
Tulir Asokan
8c57b7a69b Fix adding custom avatar URL in member metadata 2023-06-02 20:38:56 +03:00
Tulir Asokan
a265d03319 Add support for bulk message delete from Discord 2023-06-02 16:13:22 +03:00
Tulir Asokan
1c606e97a6 Enable ATX headers in Discord markdown 2023-05-31 11:45:10 +03:00
Tulir Asokan
e6108cb25d Include guild profiles in custom event field 2023-05-27 14:41:06 +03:00
Tulir Asokan
d004aea9cb Fix typo in query 2023-05-27 13:55:30 +03:00
Tulir Asokan
0fd88fedea Make mxc column non-unique for files. Fixes #71 2023-05-27 13:50:28 +03:00
Tulir Asokan
1e9099e989 Fix typo in db migration name 2023-05-27 13:44:54 +03:00
Tulir Asokan
52fa4da8b2 Reupload webhook avatars to fill custom metadata 2023-05-27 13:35:37 +03:00
Tulir Asokan
4393772ccc Add options to improve handling of webhook messages sent by other bridges 2023-05-27 13:01:24 +03:00
Tulir Asokan
824dea4745 Store global name and webhook status for puppets 2023-05-27 12:31:57 +03:00
Tulir Asokan
07182efddd Update mautrix-go 2023-05-26 15:42:47 +03:00
Tulir Asokan
280e01969a Update changelog 2023-05-25 13:25:46 +03:00
Tulir Asokan
084cde0162 Handle raw image link embeds like video gif embeds 2023-05-25 13:22:54 +03:00
Tulir Asokan
434f27c8b4 Add support for intentional mentions 2023-05-25 13:22:07 +03:00
Tulir Asokan
75181741da Update default displayname template 2023-05-22 20:32:36 +03:00
Tulir Asokan
e85f50633d Update changelog
[skip ci]
2023-05-16 18:18:44 +03:00
23 changed files with 466 additions and 190 deletions

View File

@@ -1,3 +1,20 @@
# v0.5.0 (2023-06-16)
* Added support for intentional mentions in Matrix (MSC3952).
* Added `GlobalName` variable to displayname templates and updated the default
template to prefer it over usernames.
* Added `Webhook` variable to displayname templates to allow determining if a
ghost user is a webhook.
* Added guild profiles and webhook profiles as a custom field in Matrix
message events.
* Added support for bulk message delete from Discord.
* Added support for appservice websockets.
* Enabled parsing headers (`#`) in Discord markdown.
* Messages that consist of a single image link are now bridged as images to
closer match Discord.
* Stopped bridging incoming typing notifications from users who are logged into
the bridge to prevent echoes.
# v0.4.0 (2023-05-16) # v0.4.0 (2023-05-16)
* Added bridging of friend nicks into DM room names. * Added bridging of friend nicks into DM room names.
@@ -6,12 +23,17 @@
* Added conversion of replies to embeds when sending messages via webhook. * Added conversion of replies to embeds when sending messages via webhook.
* Added option to disable caching reuploaded media. This may be necessary when * Added option to disable caching reuploaded media. This may be necessary when
using a media repo that doesn't create a unique mxc URI for each upload. using a media repo that doesn't create a unique mxc URI for each upload.
* Added option to disable uploading files directly to the Discord CDN
(and send as form parts in the message send request instead).
* Improved formatting of error messages returned by Discord. * Improved formatting of error messages returned by Discord.
* Enabled discordgo info logs by default. * Enabled discordgo info logs by default.
* Fixed limited backfill always stopping after 50 messages * Fixed limited backfill always stopping after 50 messages
(thanks to [@odrling] in [#81]). (thanks to [@odrling] in [#81]).
* Fixed startup sync to sync most recent private channels first. * Fixed startup sync to sync most recent private channels first.
* Fixed syncing group DM participants when they change. * Fixed syncing group DM participants when they change.
* Fixed bridging animated emojis in messages.
* Stopped handling all message edits from relay webhook to prevent incorrect
edits.
* Possibly fixed inviting to portal rooms when multiple Matrix users use the * Possibly fixed inviting to portal rooms when multiple Matrix users use the
bridge. bridge.

View File

@@ -128,17 +128,17 @@ func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, da
ContentType: uploadMime, ContentType: uploadMime,
} }
if br.Config.Homeserver.AsyncMedia { if br.Config.Homeserver.AsyncMedia {
resp, err := intent.UnstableCreateMXC() resp, err := intent.CreateMXC()
if err != nil { if err != nil {
return nil, err return nil, err
} }
dbFile.MXC = resp.ContentURI dbFile.MXC = resp.ContentURI
req.UnstableMXC = resp.ContentURI req.MXC = resp.ContentURI
req.UploadURL = resp.UploadURL req.UnstableUploadURL = resp.UnstableUploadURL
go func() { go func() {
_, err = intent.UploadMedia(req) _, err = intent.UploadMedia(req)
if err != nil { if err != nil {
br.Log.Errorfln("Failed to upload %s: %v", req.UnstableMXC, err) br.Log.Errorfln("Failed to upload %s: %v", req.MXC, err)
dbFile.Delete() dbFile.Delete()
} }
}() }()

View File

@@ -1,40 +0,0 @@
package main
import (
"fmt"
"io"
"net/http"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
"github.com/bwmarrin/discordgo"
)
func uploadAvatar(intent *appservice.IntentAPI, url string) (id.ContentURI, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return id.ContentURI{}, fmt.Errorf("failed to prepare request: %w", err)
}
for key, value := range discordgo.DroidImageHeaders {
req.Header.Set(key, value)
}
getResp, err := http.DefaultClient.Do(req)
if err != nil {
return id.ContentURI{}, fmt.Errorf("failed to download avatar: %w", err)
}
data, err := io.ReadAll(getResp.Body)
_ = getResp.Body.Close()
if err != nil {
return id.ContentURI{}, fmt.Errorf("failed to read avatar data: %w", err)
}
mime := http.DetectContentType(data)
resp, err := intent.UploadBytes(data, mime)
if err != nil {
return id.ContentURI{}, fmt.Errorf("failed to upload avatar to Matrix: %w", err)
}
return resp.ContentURI, nil
}

View File

@@ -134,7 +134,7 @@ func (portal *Portal) backfillLimited(log zerolog.Logger, source *User, limit in
Bool("found_all", foundAll). Bool("found_all", foundAll).
Msg("Collected messages to backfill") Msg("Collected messages to backfill")
sort.Sort(MessageSlice(messages)) sort.Sort(MessageSlice(messages))
if !foundAll { if !foundAll && after != "" {
_, err = portal.sendMatrixMessage(portal.MainIntent(), event.EventMessage, &event.MessageEventContent{ _, err = portal.sendMatrixMessage(portal.MainIntent(), event.EventMessage, &event.MessageEventContent{
MsgType: event.MsgNotice, MsgType: event.MsgNotice,
Body: "Some messages may have been missed here while the bridge was offline.", Body: "Some messages may have been missed here while the bridge was offline.",
@@ -211,13 +211,14 @@ func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, mess
for _, msg := range messages { for _, msg := range messages {
for _, mention := range msg.Mentions { for _, mention := range msg.Mentions {
puppet := portal.bridge.GetPuppetByID(mention.ID) puppet := portal.bridge.GetPuppetByID(mention.ID)
puppet.UpdateInfo(nil, mention) puppet.UpdateInfo(nil, mention, "")
} }
puppet := portal.bridge.GetPuppetByID(msg.Author.ID) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
puppet.UpdateInfo(source, msg.Author) puppet.UpdateInfo(source, msg.Author, msg.WebhookID)
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
replyTo := portal.getReplyTarget(source, "", msg.MessageReference, msg.Embeds, true) replyTo := portal.getReplyTarget(source, "", msg.MessageReference, msg.Embeds, true)
mentions := portal.convertDiscordMentions(msg, false)
ts, _ := discordgo.SnowflakeTimestamp(msg.ID) ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
log := log.With(). log := log.With().
@@ -225,13 +226,18 @@ func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, mess
Int("message_type", int(msg.Type)). Int("message_type", int(msg.Type)).
Str("author_id", msg.Author.ID). Str("author_id", msg.Author.ID).
Logger() Logger()
parts := portal.convertDiscordMessage(log.WithContext(ctx), intent, msg) parts := portal.convertDiscordMessage(log.WithContext(ctx), puppet, intent, msg)
for i, part := range parts { for i, part := range parts {
if replyTo != nil { if replyTo != nil {
part.Content.RelatesTo = &event.RelatesTo{InReplyTo: replyTo} part.Content.RelatesTo = &event.RelatesTo{InReplyTo: replyTo}
// Only set reply for first event // Only set reply for first event
replyTo = nil replyTo = nil
} }
part.Content.Mentions = mentions
// Only set mentions for first event, but keep empty object for rest
mentions = &event.Mentions{}
partName := part.AttachmentID partName := part.AttachmentID
// Always use blank part name for first part so that replies and other things // Always use blank part name for first part so that replies and other things
// can reference it without knowing about attachments. // can reference it without knowing about attachments.
@@ -262,6 +268,7 @@ func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, mess
SenderID: msg.Author.ID, SenderID: msg.Author.ID,
Timestamp: ts, Timestamp: ts,
AttachmentID: part.AttachmentID, AttachmentID: part.AttachmentID,
SenderMXID: intent.UserID,
}) })
} }
} }

View File

@@ -51,6 +51,8 @@ type BridgeConfig struct {
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"` DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"` DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
FederateRooms bool `yaml:"federate_rooms"` FederateRooms bool `yaml:"federate_rooms"`
PrefixWebhookMessages bool `yaml:"prefix_webhook_messages"`
EnableWebhookAvatars bool `yaml:"enable_webhook_avatars"`
UseDiscordCDNUpload bool `yaml:"use_discord_cdn_upload"` UseDiscordCDNUpload bool `yaml:"use_discord_cdn_upload"`
CacheMedia string `yaml:"cache_media"` CacheMedia string `yaml:"cache_media"`
@@ -287,9 +289,14 @@ func (bc BridgeConfig) FormatUsername(userID string) string {
return buffer.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 var buffer strings.Builder
_ = bc.displaynameTemplate.Execute(&buffer, user) _ = bc.displaynameTemplate.Execute(&buffer, &DisplaynameParams{user, webhook})
return buffer.String() return buffer.String()
} }

View File

@@ -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_portal_on_channel_delete")
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave") helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
helper.Copy(up.Bool, "bridge", "federate_rooms") 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", "use_discord_cdn_upload")
helper.Copy(up.Bool, "bridge", "media_patterns", "enabled") helper.Copy(up.Bool, "bridge", "media_patterns", "enabled")
helper.Copy(up.Str, "bridge", "cache_media") helper.Copy(up.Str, "bridge", "cache_media")
@@ -85,6 +87,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "encryption", "require") helper.Copy(up.Bool, "bridge", "encryption", "require")
helper.Copy(up.Bool, "bridge", "encryption", "appservice") helper.Copy(up.Bool, "bridge", "encryption", "appservice")
helper.Copy(up.Bool, "bridge", "encryption", "allow_key_sharing") helper.Copy(up.Bool, "bridge", "encryption", "allow_key_sharing")
helper.Copy(up.Bool, "bridge", "encryption", "plaintext_mentions")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outbound_on_ack") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outbound_on_ack")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "dont_store_outbound") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "dont_store_outbound")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "ratchet_on_decrypt") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "ratchet_on_decrypt")
@@ -98,6 +101,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "encryption", "rotation", "enable_custom") helper.Copy(up.Bool, "bridge", "encryption", "rotation", "enable_custom")
helper.Copy(up.Int, "bridge", "encryption", "rotation", "milliseconds") helper.Copy(up.Int, "bridge", "encryption", "rotation", "milliseconds")
helper.Copy(up.Int, "bridge", "encryption", "rotation", "messages") helper.Copy(up.Int, "bridge", "encryption", "rotation", "messages")
helper.Copy(up.Bool, "bridge", "encryption", "rotation", "disable_device_change_key_rotation")
helper.Copy(up.Str, "bridge", "provisioning", "prefix") helper.Copy(up.Str, "bridge", "provisioning", "prefix")
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" { if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {

View File

@@ -39,8 +39,8 @@ func (fq *FileQuery) Get(url string, encrypted bool) *File {
return fq.New().Scan(fq.db.QueryRow(query, url, encrypted)) return fq.New().Scan(fq.db.QueryRow(query, url, encrypted))
} }
func (fq *FileQuery) GetByMXC(mxc id.ContentURI) *File { func (fq *FileQuery) GetEmojiByMXC(mxc id.ContentURI) *File {
query := fileSelect + " WHERE mxc=$1" query := fileSelect + " WHERE mxc=$1 AND emoji_name<>'' LIMIT 1"
return fq.New().Scan(fq.db.QueryRow(query, mxc.String())) return fq.New().Scan(fq.db.QueryRow(query, mxc.String()))
} }

View File

@@ -19,7 +19,7 @@ type MessageQuery struct {
} }
const ( const (
messageSelect = "SELECT dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, dc_edit_timestamp, dc_thread_id, mxid FROM message" messageSelect = "SELECT dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, dc_edit_timestamp, dc_thread_id, mxid, sender_mxid FROM message"
) )
func (mq *MessageQuery) New() *Message { func (mq *MessageQuery) New() *Message {
@@ -99,11 +99,11 @@ func (mq *MessageQuery) MassInsert(key PortalKey, msgs []Message) {
if len(msgs) == 0 { if len(msgs) == 0 {
return return
} }
valueStringFormat := "($%d, $%d, $1, $2, $%d, $%d, $%d, $%d, $%d)" valueStringFormat := "($%d, $%d, $1, $2, $%d, $%d, $%d, $%d, $%d, $%d)"
if mq.db.Dialect == dbutil.SQLite { if mq.db.Dialect == dbutil.SQLite {
valueStringFormat = strings.ReplaceAll(valueStringFormat, "$", "?") valueStringFormat = strings.ReplaceAll(valueStringFormat, "$", "?")
} }
params := make([]interface{}, 2+len(msgs)*7) params := make([]interface{}, 2+len(msgs)*8)
placeholders := make([]string, len(msgs)) placeholders := make([]string, len(msgs))
params[0] = key.ChannelID params[0] = key.ChannelID
params[1] = key.Receiver params[1] = key.Receiver
@@ -116,7 +116,8 @@ func (mq *MessageQuery) MassInsert(key PortalKey, msgs []Message) {
params[baseIndex+4] = msg.editTimestampVal() params[baseIndex+4] = msg.editTimestampVal()
params[baseIndex+5] = msg.ThreadID params[baseIndex+5] = msg.ThreadID
params[baseIndex+6] = msg.MXID params[baseIndex+6] = msg.MXID
placeholders[i] = fmt.Sprintf(valueStringFormat, baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7) params[baseIndex+7] = msg.SenderMXID.String()
placeholders[i] = fmt.Sprintf(valueStringFormat, baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8)
} }
_, err := mq.db.Exec(fmt.Sprintf(messageMassInsertTemplate, strings.Join(placeholders, ", ")), params...) _, err := mq.db.Exec(fmt.Sprintf(messageMassInsertTemplate, strings.Join(placeholders, ", ")), params...)
if err != nil { if err != nil {
@@ -137,7 +138,8 @@ type Message struct {
EditTimestamp time.Time EditTimestamp time.Time
ThreadID string ThreadID string
MXID id.EventID MXID id.EventID
SenderMXID id.UserID
} }
func (m *Message) DiscordProtoChannelID() string { func (m *Message) DiscordProtoChannelID() string {
@@ -151,7 +153,7 @@ func (m *Message) DiscordProtoChannelID() string {
func (m *Message) Scan(row dbutil.Scannable) *Message { func (m *Message) Scan(row dbutil.Scannable) *Message {
var ts, editTS int64 var ts, editTS int64
err := row.Scan(&m.DiscordID, &m.AttachmentID, &m.Channel.ChannelID, &m.Channel.Receiver, &m.SenderID, &ts, &editTS, &m.ThreadID, &m.MXID) err := row.Scan(&m.DiscordID, &m.AttachmentID, &m.Channel.ChannelID, &m.Channel.Receiver, &m.SenderID, &ts, &editTS, &m.ThreadID, &m.MXID, &m.SenderMXID)
if err != nil { if err != nil {
if !errors.Is(err, sql.ErrNoRows) { if !errors.Is(err, sql.ErrNoRows) {
m.log.Errorln("Database scan failed:", err) m.log.Errorln("Database scan failed:", err)
@@ -173,12 +175,12 @@ func (m *Message) Scan(row dbutil.Scannable) *Message {
const messageInsertQuery = ` const messageInsertQuery = `
INSERT INTO message ( INSERT INTO message (
dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, dc_edit_timestamp, dc_thread_id, mxid dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, dc_edit_timestamp, dc_thread_id, mxid, sender_mxid
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
` `
var messageMassInsertTemplate = strings.Replace(messageInsertQuery, "($1, $2, $3, $4, $5, $6, $7, $8, $9)", "%s", 1) var messageMassInsertTemplate = strings.Replace(messageInsertQuery, "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", "%s", 1)
type MessagePart struct { type MessagePart struct {
AttachmentID string AttachmentID string
@@ -196,11 +198,11 @@ func (m *Message) MassInsertParts(msgs []MessagePart) {
if len(msgs) == 0 { if len(msgs) == 0 {
return return
} }
valueStringFormat := "($1, $%d, $2, $3, $4, $5, $6, $7, $%d)" valueStringFormat := "($1, $%d, $2, $3, $4, $5, $6, $7, $%d, $8)"
if m.db.Dialect == dbutil.SQLite { if m.db.Dialect == dbutil.SQLite {
valueStringFormat = strings.ReplaceAll(valueStringFormat, "$", "?") valueStringFormat = strings.ReplaceAll(valueStringFormat, "$", "?")
} }
params := make([]interface{}, 7+len(msgs)*2) params := make([]interface{}, 8+len(msgs)*2)
placeholders := make([]string, len(msgs)) placeholders := make([]string, len(msgs))
params[0] = m.DiscordID params[0] = m.DiscordID
params[1] = m.Channel.ChannelID params[1] = m.Channel.ChannelID
@@ -209,10 +211,11 @@ func (m *Message) MassInsertParts(msgs []MessagePart) {
params[4] = m.Timestamp.UnixMilli() params[4] = m.Timestamp.UnixMilli()
params[5] = m.editTimestampVal() params[5] = m.editTimestampVal()
params[6] = m.ThreadID params[6] = m.ThreadID
params[7] = m.SenderMXID.String()
for i, msg := range msgs { for i, msg := range msgs {
params[7+i*2] = msg.AttachmentID params[8+i*2] = msg.AttachmentID
params[7+i*2+1] = msg.MXID params[8+i*2+1] = msg.MXID
placeholders[i] = fmt.Sprintf(valueStringFormat, 7+i*2+1, 7+i*2+2) placeholders[i] = fmt.Sprintf(valueStringFormat, 8+i*2+1, 8+i*2+2)
} }
_, err := m.db.Exec(fmt.Sprintf(messageMassInsertTemplate, strings.Join(placeholders, ", ")), params...) _, err := m.db.Exec(fmt.Sprintf(messageMassInsertTemplate, strings.Join(placeholders, ", ")), params...)
if err != nil { if err != nil {
@@ -224,7 +227,7 @@ func (m *Message) MassInsertParts(msgs []MessagePart) {
func (m *Message) Insert() { func (m *Message) Insert() {
_, err := m.db.Exec(messageInsertQuery, _, err := m.db.Exec(messageInsertQuery,
m.DiscordID, m.AttachmentID, m.Channel.ChannelID, m.Channel.Receiver, m.SenderID, m.DiscordID, m.AttachmentID, m.Channel.ChannelID, m.Channel.Receiver, m.SenderID,
m.Timestamp.UnixMilli(), m.editTimestampVal(), m.ThreadID, m.MXID) m.Timestamp.UnixMilli(), m.editTimestampVal(), m.ThreadID, m.MXID, m.SenderMXID.String())
if err != nil { if err != nil {
m.log.Warnfln("Failed to insert %s@%s: %v", m.DiscordID, m.Channel, err) m.log.Warnfln("Failed to insert %s@%s: %v", m.DiscordID, m.Channel, err)

View File

@@ -11,7 +11,7 @@ import (
const ( const (
puppetSelect = "SELECT id, name, name_set, avatar, avatar_url, avatar_set," + puppetSelect = "SELECT id, name, name_set, avatar, avatar_url, avatar_set," +
" contact_info_set, username, discriminator, is_bot, custom_mxid, access_token, next_batch" + " contact_info_set, global_name, username, discriminator, is_bot, is_webhook, custom_mxid, access_token, next_batch" +
" FROM puppet " " FROM puppet "
) )
@@ -75,9 +75,11 @@ type Puppet struct {
ContactInfoSet bool ContactInfoSet bool
GlobalName string
Username string Username string
Discriminator string Discriminator string
IsBot bool IsBot bool
IsWebhook bool
CustomMXID id.UserID CustomMXID id.UserID
AccessToken string AccessToken string
@@ -89,7 +91,7 @@ func (p *Puppet) Scan(row dbutil.Scannable) *Puppet {
var customMXID, accessToken, nextBatch sql.NullString var customMXID, accessToken, nextBatch sql.NullString
err := row.Scan(&p.ID, &p.Name, &p.NameSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.ContactInfoSet, err := row.Scan(&p.ID, &p.Name, &p.NameSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.ContactInfoSet,
&p.Username, &p.Discriminator, &p.IsBot, &customMXID, &accessToken, &nextBatch) &p.GlobalName, &p.Username, &p.Discriminator, &p.IsBot, &p.IsWebhook, &customMXID, &accessToken, &nextBatch)
if err != nil { if err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
@@ -110,11 +112,16 @@ func (p *Puppet) Scan(row dbutil.Scannable) *Puppet {
func (p *Puppet) Insert() { func (p *Puppet) Insert() {
query := ` query := `
INSERT INTO puppet (id, name, name_set, avatar, avatar_url, avatar_set, contact_info_set, username, discriminator, is_bot, custom_mxid, access_token, next_batch) INSERT INTO puppet (
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) id, name, name_set, avatar, avatar_url, avatar_set, contact_info_set,
global_name, username, discriminator, is_bot, is_webhook,
custom_mxid, access_token, next_batch
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
` `
_, err := p.db.Exec(query, p.ID, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet, _, err := p.db.Exec(query, p.ID, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet,
p.Username, p.Discriminator, p.IsBot, strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch)) p.GlobalName, p.Username, p.Discriminator, p.IsBot, p.IsWebhook,
strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch))
if err != nil { if err != nil {
p.log.Warnfln("Failed to insert %s: %v", p.ID, err) p.log.Warnfln("Failed to insert %s: %v", p.ID, err)
@@ -125,12 +132,17 @@ func (p *Puppet) Insert() {
func (p *Puppet) Update() { func (p *Puppet) Update() {
query := ` query := `
UPDATE puppet SET name=$1, name_set=$2, avatar=$3, avatar_url=$4, avatar_set=$5, contact_info_set=$6, UPDATE puppet SET name=$1, name_set=$2, avatar=$3, avatar_url=$4, avatar_set=$5, contact_info_set=$6,
username=$7, discriminator=$8, is_bot=$9, custom_mxid=$10, access_token=$11, next_batch=$12 global_name=$7, username=$8, discriminator=$9, is_bot=$10, is_webhook=$11,
WHERE id=$13 custom_mxid=$12, access_token=$13, next_batch=$14
WHERE id=$15
` `
_, err := p.db.Exec(query, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet, _, err := p.db.Exec(
p.Username, p.Discriminator, p.IsBot, strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch), query,
p.ID) p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet,
p.GlobalName, p.Username, p.Discriminator, p.IsBot, p.IsWebhook,
strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch),
p.ID,
)
if err != nil { if err != nil {
p.log.Warnfln("Failed to update %s: %v", p.ID, err) p.log.Warnfln("Failed to update %s: %v", p.ID, err)

View File

@@ -1,4 +1,4 @@
-- v0 -> v19: Latest revision -- v0 -> v22 (compatible with v19+): Latest revision
CREATE TABLE guild ( CREATE TABLE guild (
dcid TEXT PRIMARY KEY, dcid TEXT PRIMARY KEY,
@@ -71,9 +71,11 @@ CREATE TABLE puppet (
contact_info_set BOOLEAN NOT NULL DEFAULT false, contact_info_set BOOLEAN NOT NULL DEFAULT false,
global_name TEXT NOT NULL DEFAULT '',
username TEXT NOT NULL DEFAULT '', username TEXT NOT NULL DEFAULT '',
discriminator TEXT NOT NULL DEFAULT '', discriminator TEXT NOT NULL DEFAULT '',
is_bot BOOLEAN NOT NULL DEFAULT false, is_bot BOOLEAN NOT NULL DEFAULT false,
is_webhook BOOLEAN NOT NULL DEFAULT false,
custom_mxid TEXT, custom_mxid TEXT,
access_token TEXT, access_token TEXT,
@@ -113,7 +115,8 @@ CREATE TABLE message (
dc_edit_timestamp BIGINT NOT NULL, dc_edit_timestamp BIGINT NOT NULL,
dc_thread_id TEXT NOT NULL, dc_thread_id TEXT NOT NULL,
mxid TEXT NOT NULL UNIQUE, mxid TEXT NOT NULL UNIQUE,
sender_mxid TEXT NOT NULL DEFAULT '',
PRIMARY KEY (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver), PRIMARY KEY (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver),
CONSTRAINT message_portal_fkey FOREIGN KEY (dc_chan_id, dc_chan_receiver) REFERENCES portal (dcid, receiver) ON DELETE CASCADE CONSTRAINT message_portal_fkey FOREIGN KEY (dc_chan_id, dc_chan_receiver) REFERENCES portal (dcid, receiver) ON DELETE CASCADE
@@ -157,7 +160,7 @@ CREATE TABLE role (
CREATE TABLE discord_file ( CREATE TABLE discord_file (
url TEXT, url TEXT,
encrypted BOOLEAN, encrypted BOOLEAN,
mxc TEXT NOT NULL UNIQUE, mxc TEXT NOT NULL,
id TEXT, id TEXT,
emoji_name TEXT, emoji_name TEXT,
@@ -171,3 +174,5 @@ CREATE TABLE discord_file (
PRIMARY KEY (url, encrypted) PRIMARY KEY (url, encrypted)
); );
CREATE INDEX discord_file_mxc_idx ON discord_file (mxc);

View File

@@ -0,0 +1,2 @@
-- v20 (compatible with v19+): Store message sender Matrix user ID
ALTER TABLE message ADD COLUMN sender_mxid TEXT NOT NULL DEFAULT '';

View File

@@ -0,0 +1,3 @@
-- v21 (compatible with v19+): Store global displayname and is webhook status for puppets
ALTER TABLE puppet ADD COLUMN global_name TEXT NOT NULL DEFAULT '';
ALTER TABLE puppet ADD COLUMN is_webhook BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,26 @@
-- v22 (compatible with v19+): Allow non-unique mxc URIs in file cache
CREATE TABLE new_discord_file (
url TEXT,
encrypted BOOLEAN,
mxc TEXT NOT NULL,
id TEXT,
emoji_name TEXT,
size BIGINT NOT NULL,
width INTEGER,
height INTEGER,
mime_type TEXT NOT NULL,
decryption_info jsonb,
timestamp BIGINT NOT NULL,
PRIMARY KEY (url, encrypted)
);
INSERT INTO new_discord_file (url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp)
SELECT url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp FROM discord_file;
DROP TABLE discord_file;
ALTER TABLE new_discord_file RENAME TO discord_file;
CREATE INDEX discord_file_mxc_idx ON discord_file (mxc);

View File

@@ -20,6 +20,13 @@ homeserver:
# Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?
async_media: false async_media: false
# Should the bridge use a websocket for connecting to the homeserver?
# The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,
# mautrix-asmux (deprecated), and hungryserv (proprietary).
websocket: false
# How often should the websocket be pinged? Pinging will be disabled if this is zero.
ping_interval_seconds: 0
# Application service host/registration related details. # Application service host/registration related details.
# Changing these values requires regeneration of the registration. # Changing these values requires regeneration of the registration.
appservice: appservice:
@@ -80,11 +87,13 @@ bridge:
# Displayname template for Discord users. This is also used as the room name in DMs if private_chat_portal_meta is enabled. # Displayname template for Discord users. This is also used as the room name in DMs if private_chat_portal_meta is enabled.
# Available variables: # Available variables:
# .ID - Internal user ID # .ID - Internal user ID
# .Username - User's displayname on Discord # .Username - Legacy display/username on Discord
# .GlobalName - New displayname on Discord
# .Discriminator - The 4 numbers after the name on Discord # .Discriminator - The 4 numbers after the name on Discord
# .Bot - Whether the user is a bot # .Bot - Whether the user is a bot
# .System - Whether the user is an official system user # .System - Whether the user is an official system user
displayname_template: '{{.Username}}#{{.Discriminator}}{{if .Bot}} (bot){{end}}' # .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). # Displayname template for Discord channels (bridged as rooms, or spaces when type=4).
# Available variables: # Available variables:
# .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs. # .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs.
@@ -145,6 +154,11 @@ bridge:
# Whether or not created rooms should have federation enabled. # Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated. # If false, created portal rooms will never be federated.
federate_rooms: true 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, # 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 # 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). # (which is always used by bots and webhooks).
@@ -246,6 +260,8 @@ bridge:
# Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.
# You must use a client that supports requesting keys from other users to use this feature. # You must use a client that supports requesting keys from other users to use this feature.
allow_key_sharing: false allow_key_sharing: false
# Should users mentions be in the event wire content to enable the server to send push notifications?
plaintext_mentions: false
# Options for deleting megolm sessions from the bridge. # Options for deleting megolm sessions from the bridge.
delete_keys: delete_keys:
# Beeper-specific: delete outbound sessions when hungryserv confirms # Beeper-specific: delete outbound sessions when hungryserv confirms
@@ -298,6 +314,10 @@ bridge:
# default. # default.
messages: 100 messages: 100
# Disable rotating keys when a user's devices change?
# You should not enable this option unless you understand all the implications.
disable_device_change_key_rotation: false
# Settings for provisioning API # Settings for provisioning API
provisioning: provisioning:
# Prefix for the provisioning API paths. # Prefix for the provisioning API paths.

View File

@@ -26,6 +26,7 @@ import (
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
@@ -58,7 +59,7 @@ func (b *indentableParagraphParser) CanAcceptIndentedLine() bool {
var removeFeaturesExceptLinks = []any{ var removeFeaturesExceptLinks = []any{
parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(), parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
parser.NewSetextHeadingParser(), parser.NewATXHeadingParser(), parser.NewThematicBreakParser(), parser.NewSetextHeadingParser(), parser.NewThematicBreakParser(),
parser.NewCodeBlockParser(), parser.NewCodeBlockParser(),
} }
var removeFeaturesAndLinks = append(removeFeaturesExceptLinks, parser.NewLinkParser()) var removeFeaturesAndLinks = append(removeFeaturesExceptLinks, parser.NewLinkParser())
@@ -93,6 +94,7 @@ func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLink
const formatterContextPortalKey = "fi.mau.discord.portal" const formatterContextPortalKey = "fi.mau.discord.portal"
const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions" const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions"
const formatterContextInputAllowedMentionsKey = "fi.mau.discord.input_allowed_mentions"
func appendIfNotContains(arr []string, newItem string) []string { func appendIfNotContains(arr []string, newItem string) []string {
for _, item := range arr { for _, item := range arr {
@@ -135,6 +137,10 @@ func (br *DiscordBridge) pillConverter(displayname, mxid, eventID string, ctx fo
} }
} }
} else if mxid[0] == '@' { } else if mxid[0] == '@' {
allowedMentions, _ := ctx.ReturnData[formatterContextInputAllowedMentionsKey].([]id.UserID)
if allowedMentions != nil && !slices.Contains(allowedMentions, id.UserID(mxid)) {
return displayname
}
mentions := ctx.ReturnData[formatterContextAllowedMentionsKey].(*discordgo.MessageAllowedMentions) mentions := ctx.ReturnData[formatterContextAllowedMentionsKey].(*discordgo.MessageAllowedMentions)
parsedID, ok := br.ParsePuppetMXID(id.UserID(mxid)) parsedID, ok := br.ParsePuppetMXID(id.UserID(mxid))
if ok { if ok {
@@ -164,6 +170,7 @@ var discordMarkdownEscaper = strings.NewReplacer(
"`", "\\`", "`", "\\`",
`|`, `\|`, `|`, `\|`,
`<`, `\<`, `<`, `\<`,
`#`, `\#`,
) )
func escapeDiscordMarkdown(s string) string { func escapeDiscordMarkdown(s string) string {
@@ -219,6 +226,9 @@ func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) (strin
ctx := format.NewContext() ctx := format.NewContext()
ctx.ReturnData[formatterContextPortalKey] = portal ctx.ReturnData[formatterContextPortalKey] = portal
ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions
if content.Mentions != nil {
ctx.ReturnData[formatterContextInputAllowedMentionsKey] = content.Mentions.UserIDs
}
return variationselector.FullyQualify(matrixHTMLParser.Parse(content.FormattedBody, ctx)), allowedMentions return variationselector.FullyQualify(matrixHTMLParser.Parse(content.FormattedBody, ctx)), allowedMentions
} else { } else {
return variationselector.FullyQualify(escapeDiscordMarkdown(content.Body)), allowedMentions return variationselector.FullyQualify(escapeDiscordMarkdown(content.Body)), allowedMentions

16
go.mod
View File

@@ -9,13 +9,14 @@ require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.17
github.com/rs/zerolog v1.29.1 github.com/rs/zerolog v1.29.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.4
github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark v1.5.4
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
maunium.net/go/maulogger/v2 v2.4.1 maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.15.2 maunium.net/go/mautrix v0.15.3
) )
require ( require (
@@ -29,13 +30,12 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/zeroconfig v0.1.2 // indirect go.mau.fi/zeroconfig v0.1.2 // indirect
golang.org/x/crypto v0.9.0 // indirect golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect golang.org/x/net v0.11.0 // indirect
golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect
) )
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230426184739-79aea97f6660 replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230512133900-5b12693331c0

39
go.sum
View File

@@ -1,9 +1,8 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/beeper/discordgo v0.0.0-20230426184739-79aea97f6660 h1:5LFnUY/Aj/0k/UqeEmW2GS4ql1vxmivkrckPxUHf8oc= github.com/beeper/discordgo v0.0.0-20230512133900-5b12693331c0 h1:ECBEbC4ruaXzcVJJ4UurkGpT/Xlm9ZnwsHiHn9gjPZw=
github.com/beeper/discordgo v0.0.0-20230426184739-79aea97f6660/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA= github.com/beeper/discordgo v0.0.0-20230512133900-5b12693331c0/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
@@ -21,8 +20,8 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -31,13 +30,8 @@ github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -51,26 +45,25 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= 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/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 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU= maunium.net/go/mautrix v0.15.3 h1:C9BHSUM0gYbuZmAtopuLjIcH5XHLb/ZjTEz7nN+0jN0=
maunium.net/go/mautrix v0.15.2/go.mod h1:h4NwfKqE4YxGTLSgn/gawKzXAb2sF4qx8agL6QEFtGg= maunium.net/go/mautrix v0.15.3/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE=

View File

@@ -272,12 +272,15 @@ func (guild *Guild) UpdateAvatar(iconID string) bool {
guild.Avatar = iconID guild.Avatar = iconID
guild.AvatarURL = id.ContentURI{} guild.AvatarURL = id.ContentURI{}
if guild.Avatar != "" { if guild.Avatar != "" {
var err error // TODO direct media support
guild.AvatarURL, err = uploadAvatar(guild.bridge.Bot, discordgo.EndpointGuildIcon(guild.ID, iconID)) copied, err := guild.bridge.copyAttachmentToMatrix(guild.bridge.Bot, discordgo.EndpointGuildIcon(guild.ID, iconID), false, AttachmentMeta{
AttachmentID: fmt.Sprintf("guild_avatar/%s/%s", guild.ID, iconID),
})
if err != nil { if err != nil {
guild.log.Warnfln("Failed to reupload guild avatar %s: %v", guild.Avatar, err) guild.log.Warnfln("Failed to reupload guild avatar %s: %v", iconID, err)
return true return true
} }
guild.AvatarURL = copied.MXC
} }
if guild.MXID != "" { if guild.MXID != "" {
_, err := guild.bridge.Bot.SetRoomAvatar(guild.MXID, guild.AvatarURL) _, err := guild.bridge.Bot.SetRoomAvatar(guild.MXID, guild.AvatarURL)

View File

@@ -102,7 +102,7 @@ func (br *DiscordBridge) Start() {
if br.Config.Bridge.Provisioning.SharedSecret != "disable" { if br.Config.Bridge.Provisioning.SharedSecret != "disable" {
br.provisioning = newProvisioningAPI(br) br.provisioning = newProvisioningAPI(br)
} }
go br.updatePuppetsContactInfo() br.WaitWebsocketConnected()
go br.startUsers() go br.startUsers()
} }
@@ -176,7 +176,7 @@ func main() {
Name: "mautrix-discord", Name: "mautrix-discord",
URL: "https://github.com/mautrix/discord", URL: "https://github.com/mautrix/discord",
Description: "A Matrix-Discord puppeting bridge.", Description: "A Matrix-Discord puppeting bridge.",
Version: "0.4.0", Version: "0.5.0",
ProtocolName: "Discord", ProtocolName: "Discord",
BeeperServiceName: "discordgo", BeeperServiceName: "discordgo",
BeeperNetworkName: "discord", BeeperNetworkName: "discord",

View File

@@ -571,6 +571,8 @@ func (portal *Portal) handleDiscordMessages(msg portalDiscordMessage) {
portal.handleDiscordMessageUpdate(msg.user, convertedMsg.Message) portal.handleDiscordMessageUpdate(msg.user, convertedMsg.Message)
case *discordgo.MessageDelete: case *discordgo.MessageDelete:
portal.handleDiscordMessageDelete(msg.user, convertedMsg.Message) portal.handleDiscordMessageDelete(msg.user, convertedMsg.Message)
case *discordgo.MessageDeleteBulk:
portal.handleDiscordMessageDeleteBulk(msg.user, convertedMsg.Messages)
case *discordgo.MessageReactionAdd: case *discordgo.MessageReactionAdd:
portal.handleDiscordReaction(msg.user, convertedMsg.MessageReaction, true, msg.thread, convertedMsg.Member) portal.handleDiscordReaction(msg.user, convertedMsg.MessageReaction, true, msg.thread, convertedMsg.Member)
case *discordgo.MessageReactionRemove: case *discordgo.MessageReactionRemove:
@@ -584,13 +586,14 @@ func (portal *Portal) ensureUserInvited(user *User, ignoreCache bool) bool {
return user.ensureInvited(portal.MainIntent(), portal.MXID, portal.IsPrivateChat(), ignoreCache) return user.ensureInvited(portal.MainIntent(), portal.MXID, portal.IsPrivateChat(), ignoreCache)
} }
func (portal *Portal) markMessageHandled(discordID string, authorID string, timestamp time.Time, threadID string, parts []database.MessagePart) { func (portal *Portal) markMessageHandled(discordID string, authorID string, timestamp time.Time, threadID string, senderMXID id.UserID, parts []database.MessagePart) {
msg := portal.bridge.DB.Message.New() msg := portal.bridge.DB.Message.New()
msg.Channel = portal.Key msg.Channel = portal.Key
msg.DiscordID = discordID msg.DiscordID = discordID
msg.SenderID = authorID msg.SenderID = authorID
msg.Timestamp = timestamp msg.Timestamp = timestamp
msg.ThreadID = threadID msg.ThreadID = threadID
msg.SenderMXID = senderMXID
msg.MassInsertParts(parts) msg.MassInsertParts(parts)
} }
@@ -618,13 +621,8 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
} }
log.Debug().Msg("Starting handling of Discord message") log.Debug().Msg("Starting handling of Discord message")
for _, mention := range msg.Mentions {
puppet := portal.bridge.GetPuppetByID(mention.ID)
puppet.UpdateInfo(nil, mention)
}
puppet := portal.bridge.GetPuppetByID(msg.Author.ID) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
puppet.UpdateInfo(user, msg.Author) puppet.UpdateInfo(user, msg.Author, msg.WebhookID)
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
var discordThreadID string var discordThreadID string
@@ -639,9 +637,10 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
} }
} }
replyTo := portal.getReplyTarget(user, discordThreadID, msg.MessageReference, msg.Embeds, false) replyTo := portal.getReplyTarget(user, discordThreadID, msg.MessageReference, msg.Embeds, false)
mentions := portal.convertDiscordMentions(msg, true)
ts, _ := discordgo.SnowflakeTimestamp(msg.ID) ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
parts := portal.convertDiscordMessage(ctx, intent, msg) parts := portal.convertDiscordMessage(ctx, puppet, intent, msg)
dbParts := make([]database.MessagePart, 0, len(parts)) dbParts := make([]database.MessagePart, 0, len(parts))
for i, part := range parts { for i, part := range parts {
if (replyTo != nil || threadRootEvent != "") && part.Content.RelatesTo == nil { if (replyTo != nil || threadRootEvent != "") && part.Content.RelatesTo == nil {
@@ -658,6 +657,11 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
// Only set reply for first event // Only set reply for first event
replyTo = nil replyTo = nil
} }
part.Content.Mentions = mentions
// Only set mentions for first event, but keep empty object for rest
mentions = &event.Mentions{}
resp, err := portal.sendMatrixMessage(intent, part.Type, part.Content, part.Extra, ts.UnixMilli()) resp, err := portal.sendMatrixMessage(intent, part.Type, part.Content, part.Extra, ts.UnixMilli())
if err != nil { if err != nil {
log.Err(err). log.Err(err).
@@ -674,7 +678,7 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
} else if len(dbParts) == 0 { } else if len(dbParts) == 0 {
log.Warn().Msg("All parts of message failed to send to Matrix") log.Warn().Msg("All parts of message failed to send to Matrix")
} else { } else {
portal.markMessageHandled(msg.ID, msg.Author.ID, ts, discordThreadID, dbParts) portal.markMessageHandled(msg.ID, msg.Author.ID, ts, discordThreadID, intent.UserID, dbParts)
} }
} }
@@ -842,7 +846,8 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
return return
} }
intent := portal.bridge.GetPuppetByID(msg.Author.ID).IntentFor(portal) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
intent := puppet.IntentFor(portal)
attachmentMap := map[string]*database.Message{} attachmentMap := map[string]*database.Message{}
for _, existingPart := range existing { for _, existingPart := range existing {
@@ -862,7 +867,7 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
} }
for _, remainingEmbed := range msg.Embeds { for _, remainingEmbed := range msg.Embeds {
// Other types of embeds are sent inline with the text message part // Other types of embeds are sent inline with the text message part
if getEmbedType(remainingEmbed) != EmbedVideo { if getEmbedType(nil, remainingEmbed) != EmbedVideo {
continue continue
} }
embedID := "video_" + remainingEmbed.URL embedID := "video_" + remainingEmbed.URL
@@ -895,7 +900,12 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
Msg("Dropping non-text edit") Msg("Dropping non-text edit")
return return
} }
puppet.addWebhookMeta(converted, msg)
puppet.addMemberMeta(converted, msg)
converted.Content.Mentions = portal.convertDiscordMentions(msg, false)
converted.Content.SetEdit(existing[0].MXID) converted.Content.SetEdit(existing[0].MXID)
// Never actually mention new users of edits, only include mentions inside m.new_content
converted.Content.Mentions = &event.Mentions{}
if converted.Extra != nil { if converted.Extra != nil {
converted.Extra = map[string]any{ converted.Extra = map[string]any{
"m.new_content": converted.Extra, "m.new_content": converted.Extra,
@@ -921,14 +931,33 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
} }
func (portal *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Message) { func (portal *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Message) {
existing := portal.bridge.DB.Message.GetByDiscordID(portal.Key, msg.ID) lastResp := portal.redactAllParts(portal.MainIntent(), msg.ID)
if lastResp != "" {
portal.sendDeliveryReceipt(lastResp)
}
}
func (portal *Portal) handleDiscordMessageDeleteBulk(user *User, messages []string) {
intent := portal.MainIntent() intent := portal.MainIntent()
var lastResp id.EventID var lastResp id.EventID
for _, msgID := range messages {
newLastResp := portal.redactAllParts(intent, msgID)
if newLastResp != "" {
lastResp = newLastResp
}
}
if lastResp != "" {
portal.sendDeliveryReceipt(lastResp)
}
}
func (portal *Portal) redactAllParts(intent *appservice.IntentAPI, msgID string) (lastResp id.EventID) {
existing := portal.bridge.DB.Message.GetByDiscordID(portal.Key, msgID)
for _, dbMsg := range existing { for _, dbMsg := range existing {
resp, err := intent.RedactEvent(portal.MXID, dbMsg.MXID) resp, err := intent.RedactEvent(portal.MXID, dbMsg.MXID)
if err != nil { if err != nil {
portal.log.Err(err). portal.log.Err(err).
Str("message_id", msg.ID). Str("message_id", msgID).
Str("event_id", dbMsg.MXID.String()). Str("event_id", dbMsg.MXID.String()).
Msg("Failed to redact Matrix message") Msg("Failed to redact Matrix message")
} else if resp != nil && resp.EventID != "" { } else if resp != nil && resp.EventID != "" {
@@ -936,9 +965,7 @@ func (portal *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Mess
} }
dbMsg.Delete() dbMsg.Delete()
} }
if lastResp != "" { return
portal.sendDeliveryReceipt(lastResp)
}
} }
func (portal *Portal) handleDiscordTyping(evt *discordgo.TypingStart) { func (portal *Portal) handleDiscordTyping(evt *discordgo.TypingStart) {
@@ -965,7 +992,7 @@ func (portal *Portal) handleDiscordTyping(evt *discordgo.TypingStart) {
func (portal *Portal) syncParticipant(source *User, participant *discordgo.User, remove bool) { func (portal *Portal) syncParticipant(source *User, participant *discordgo.User, remove bool) {
puppet := portal.bridge.GetPuppetByID(participant.ID) puppet := portal.bridge.GetPuppetByID(participant.ID)
puppet.UpdateInfo(source, participant) puppet.UpdateInfo(source, participant, "")
log := portal.log.With(). log := portal.log.With().
Str("participant_id", participant.ID). Str("participant_id", participant.ID).
Str("ghost_mxid", puppet.MXID.String()). Str("ghost_mxid", puppet.MXID.String()).
@@ -992,7 +1019,7 @@ func (portal *Portal) syncParticipant(source *User, participant *discordgo.User,
func (portal *Portal) syncParticipants(source *User, participants []*discordgo.User) { func (portal *Portal) syncParticipants(source *User, participants []*discordgo.User) {
for _, participant := range participants { for _, participant := range participants {
puppet := portal.bridge.GetPuppetByID(participant.ID) puppet := portal.bridge.GetPuppetByID(participant.ID)
puppet.UpdateInfo(source, participant) puppet.UpdateInfo(source, participant, "")
user := portal.bridge.GetUserByID(participant.ID) user := portal.bridge.GetUserByID(participant.ID)
if user != nil { if user != nil {
@@ -1585,6 +1612,7 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
} else { } else {
dbMsg.SenderID = portal.RelayWebhookID dbMsg.SenderID = portal.RelayWebhookID
} }
dbMsg.SenderMXID = sender.MXID
dbMsg.Timestamp, _ = discordgo.SnowflakeTimestamp(msg.ID) dbMsg.Timestamp, _ = discordgo.SnowflakeTimestamp(msg.ID)
dbMsg.ThreadID = threadID dbMsg.ThreadID = threadID
dbMsg.Insert() dbMsg.Insert()
@@ -1781,7 +1809,7 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) {
emojiID := reaction.RelatesTo.Key emojiID := reaction.RelatesTo.Key
if strings.HasPrefix(emojiID, "mxc://") { if strings.HasPrefix(emojiID, "mxc://") {
uri, _ := id.ParseContentURI(emojiID) uri, _ := id.ParseContentURI(emojiID)
emojiFile := portal.bridge.DB.File.GetByMXC(uri) emojiFile := portal.bridge.DB.File.GetEmojiByMXC(uri)
if emojiFile == nil || emojiFile.ID == "" || emojiFile.EmojiName == "" { if emojiFile == nil || emojiFile.ID == "" || emojiFile.EmojiName == "" {
go portal.sendMessageMetrics(evt, fmt.Errorf("%w %s", errUnknownEmoji, emojiID), "Ignoring") go portal.sendMessageMetrics(evt, fmt.Errorf("%w %s", errUnknownEmoji, emojiID), "Ignoring")
return return
@@ -1820,7 +1848,7 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) {
func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageReaction, add bool, thread *Thread, member *discordgo.Member) { func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageReaction, add bool, thread *Thread, member *discordgo.Member) {
puppet := portal.bridge.GetPuppetByID(reaction.UserID) puppet := portal.bridge.GetPuppetByID(reaction.UserID)
if member != nil { if member != nil {
puppet.UpdateInfo(user, member.User) puppet.UpdateInfo(user, member.User, "")
} }
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
@@ -2143,13 +2171,15 @@ func (portal *Portal) UpdateGroupDMAvatar(iconID string) bool {
portal.AvatarSet = false portal.AvatarSet = false
portal.AvatarURL = id.ContentURI{} portal.AvatarURL = id.ContentURI{}
if portal.Avatar != "" { if portal.Avatar != "" {
uri, err := uploadAvatar(portal.MainIntent(), discordgo.EndpointGroupIcon(portal.Key.ChannelID, portal.Avatar)) // TODO direct media support
copied, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), discordgo.EndpointGroupIcon(portal.Key.ChannelID, portal.Avatar), false, AttachmentMeta{
AttachmentID: fmt.Sprintf("private_channel_avatar/%s/%s", portal.Key.ChannelID, iconID),
})
if err != nil { if err != nil {
portal.log.Err(err).Str("avatar_id", portal.Avatar).Msg("Failed to reupload channel avatar") portal.log.Err(err).Str("avatar_id", iconID).Msg("Failed to reupload channel avatar")
return true return true
} else {
portal.AvatarURL = uri
} }
portal.AvatarURL = copied.MXC
} }
portal.updateRoomAvatar() portal.updateRoomAvatar()
return true return true

View File

@@ -26,6 +26,8 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
@@ -193,7 +195,13 @@ func (portal *Portal) convertDiscordAttachment(ctx context.Context, intent *apps
func (portal *Portal) convertDiscordVideoEmbed(ctx context.Context, intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *ConvertedMessage { func (portal *Portal) convertDiscordVideoEmbed(ctx context.Context, intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *ConvertedMessage {
attachmentID := fmt.Sprintf("video_%s", embed.URL) attachmentID := fmt.Sprintf("video_%s", embed.URL)
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, NoMeta) var proxyURL string
if embed.Video != nil {
proxyURL = embed.Video.ProxyURL
} else {
proxyURL = embed.Thumbnail.ProxyURL
}
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, proxyURL, portal.Encrypted, NoMeta)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix") zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix")
return &ConvertedMessage{ return &ConvertedMessage{
@@ -204,16 +212,21 @@ func (portal *Portal) convertDiscordVideoEmbed(ctx context.Context, intent *apps
} }
content := &event.MessageEventContent{ content := &event.MessageEventContent{
MsgType: event.MsgVideo, Body: embed.URL,
Body: embed.URL,
Info: &event.FileInfo{ Info: &event.FileInfo{
Width: embed.Video.Width,
Height: embed.Video.Height,
MimeType: dbFile.MimeType, MimeType: dbFile.MimeType,
Size: dbFile.Size,
Size: dbFile.Size,
}, },
} }
if embed.Video != nil {
content.MsgType = event.MsgVideo
content.Info.Width = embed.Video.Width
content.Info.Height = embed.Video.Height
} else {
content.MsgType = event.MsgImage
content.Info.Width = embed.Thumbnail.Width
content.Info.Height = embed.Thumbnail.Height
}
if content.Info.Width == 0 && content.Info.Height == 0 { if content.Info.Width == 0 && content.Info.Height == 0 {
content.Info.Width = dbFile.Width content.Info.Width = dbFile.Width
content.Info.Height = dbFile.Height content.Info.Height = dbFile.Height
@@ -227,7 +240,7 @@ func (portal *Portal) convertDiscordVideoEmbed(ctx context.Context, intent *apps
content.URL = dbFile.MXC.CUString() content.URL = dbFile.MXC.CUString()
} }
extra := map[string]any{} extra := map[string]any{}
if embed.Type == discordgo.EmbedTypeGifv { if content.MsgType == event.MsgVideo && embed.Type == discordgo.EmbedTypeGifv {
extra["info"] = map[string]any{ extra["info"] = map[string]any{
"fi.mau.discord.gifv": true, "fi.mau.discord.gifv": true,
"fi.mau.loop": true, "fi.mau.loop": true,
@@ -244,7 +257,7 @@ func (portal *Portal) convertDiscordVideoEmbed(ctx context.Context, intent *apps
} }
} }
func (portal *Portal) convertDiscordMessage(ctx context.Context, intent *appservice.IntentAPI, msg *discordgo.Message) []*ConvertedMessage { func (portal *Portal) convertDiscordMessage(ctx context.Context, puppet *Puppet, intent *appservice.IntentAPI, msg *discordgo.Message) []*ConvertedMessage {
predictedLength := len(msg.Attachments) + len(msg.StickerItems) predictedLength := len(msg.Attachments) + len(msg.StickerItems)
if msg.Content != "" { if msg.Content != "" {
predictedLength++ predictedLength++
@@ -277,7 +290,7 @@ func (portal *Portal) convertDiscordMessage(ctx context.Context, intent *appserv
} }
for i, embed := range msg.Embeds { for i, embed := range msg.Embeds {
// Ignore non-video embeds, they're handled in convertDiscordTextMessage // Ignore non-video embeds, they're handled in convertDiscordTextMessage
if getEmbedType(embed) != EmbedVideo { if getEmbedType(msg, embed) != EmbedVideo {
continue continue
} }
// Discord deduplicates embeds by URL. It makes things easier for us too. // Discord deduplicates embeds by URL. It makes things easier for us too.
@@ -295,9 +308,90 @@ func (portal *Portal) convertDiscordMessage(ctx context.Context, intent *appserv
parts = append(parts, part) parts = append(parts, part)
} }
} }
for _, part := range parts {
puppet.addWebhookMeta(part, msg)
puppet.addMemberMeta(part, msg)
}
return parts return parts
} }
func (puppet *Puppet) addMemberMeta(part *ConvertedMessage, msg *discordgo.Message) {
if msg.Member == nil {
return
}
if part.Extra == nil {
part.Extra = make(map[string]any)
}
var avatarURL id.ContentURI
if msg.Member.Avatar != "" {
var err error
avatarURL, err = puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), msg.GuildID, msg.Author.ID, msg.Author.Avatar)
if err != nil {
puppet.log.Warn().Err(err).
Str("avatar_id", msg.Author.Avatar).
Msg("Failed to reupload guild user avatar")
}
}
var discordAvararURL string
if msg.Member.Avatar != "" {
msg.Member.User = msg.Author
discordAvararURL = msg.Member.AvatarURL("")
}
part.Extra["fi.mau.discord.guild_member_metadata"] = map[string]any{
"nick": msg.Member.Nick,
"avatar_id": msg.Member.Avatar,
"avatar_url": discordAvararURL,
"avatar_mxc": avatarURL.String(),
}
if msg.Member.Nick != "" || !avatarURL.IsEmpty() {
perMessageProfile := map[string]any{
"is_multiple_users": false,
"displayname": msg.Member.Nick,
"avatar_url": avatarURL.String(),
}
if msg.Member.Nick == "" {
perMessageProfile["displayname"] = puppet.Name
}
if avatarURL.IsEmpty() {
perMessageProfile["avatar_url"] = puppet.AvatarURL.String()
}
part.Extra["com.beeper.per_message_profile"] = perMessageProfile
}
}
func (puppet *Puppet) addWebhookMeta(part *ConvertedMessage, msg *discordgo.Message) {
if msg.WebhookID == "" {
return
}
if part.Extra == nil {
part.Extra = make(map[string]any)
}
var avatarURL id.ContentURI
if msg.Author.Avatar != "" {
var err error
avatarURL, err = puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), "", msg.Author.ID, msg.Author.Avatar)
if err != nil {
puppet.log.Warn().Err(err).
Str("avatar_id", msg.Author.Avatar).
Msg("Failed to reupload webhook avatar")
}
}
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(""),
"avatar_mxc": avatarURL.String(),
}
part.Extra["com.beeper.per_message_profile"] = map[string]any{
"is_multiple_users": true,
"avatar_url": avatarURL.String(),
"displayname": msg.Author.Username,
}
}
const ( const (
embedHTMLWrapper = `<blockquote class="discord-embed">%s</blockquote>` embedHTMLWrapper = `<blockquote class="discord-embed">%s</blockquote>`
embedHTMLWrapperColor = `<blockquote class="discord-embed" background-color="#%06X">%s</blockquote>` embedHTMLWrapperColor = `<blockquote class="discord-embed" background-color="#%06X">%s</blockquote>`
@@ -496,7 +590,7 @@ func isActuallyLinkPreview(embed *discordgo.MessageEmbed) bool {
return embed.Video != nil && embed.Video.ProxyURL == "" return embed.Video != nil && embed.Video.ProxyURL == ""
} }
func getEmbedType(embed *discordgo.MessageEmbed) BridgeEmbedType { func getEmbedType(msg *discordgo.Message, embed *discordgo.MessageEmbed) BridgeEmbedType {
switch embed.Type { switch embed.Type {
case discordgo.EmbedTypeLink, discordgo.EmbedTypeArticle: case discordgo.EmbedTypeLink, discordgo.EmbedTypeArticle:
return EmbedLinkPreview return EmbedLinkPreview
@@ -507,7 +601,14 @@ func getEmbedType(embed *discordgo.MessageEmbed) BridgeEmbedType {
return EmbedVideo return EmbedVideo
case discordgo.EmbedTypeGifv: case discordgo.EmbedTypeGifv:
return EmbedVideo return EmbedVideo
case discordgo.EmbedTypeRich, discordgo.EmbedTypeImage: case discordgo.EmbedTypeImage:
if msg != nil && isPlainGifMessage(msg) {
return EmbedVideo
} else if embed.Image == nil && embed.Thumbnail != nil {
return EmbedLinkPreview
}
return EmbedRich
case discordgo.EmbedTypeRich:
return EmbedRich return EmbedRich
default: default:
return EmbedUnknown return EmbedUnknown
@@ -515,7 +616,31 @@ func getEmbedType(embed *discordgo.MessageEmbed) BridgeEmbedType {
} }
func isPlainGifMessage(msg *discordgo.Message) bool { func isPlainGifMessage(msg *discordgo.Message) bool {
return len(msg.Embeds) == 1 && msg.Embeds[0].Video != nil && msg.Embeds[0].URL == msg.Content && msg.Embeds[0].Type == discordgo.EmbedTypeGifv return len(msg.Embeds) == 1 && msg.Embeds[0].URL == msg.Content &&
((msg.Embeds[0].Type == discordgo.EmbedTypeGifv && msg.Embeds[0].Video != nil) ||
(msg.Embeds[0].Type == discordgo.EmbedTypeImage && msg.Embeds[0].Image == nil && msg.Embeds[0].Thumbnail != nil))
}
func (portal *Portal) convertDiscordMentions(msg *discordgo.Message, syncGhosts bool) *event.Mentions {
var matrixMentions event.Mentions
for _, mention := range msg.Mentions {
puppet := portal.bridge.GetPuppetByID(mention.ID)
if syncGhosts {
puppet.UpdateInfo(nil, mention, "")
}
user := portal.bridge.GetUserByID(mention.ID)
if user != nil {
matrixMentions.UserIDs = append(matrixMentions.UserIDs, user.MXID)
} else {
matrixMentions.UserIDs = append(matrixMentions.UserIDs, puppet.MXID)
}
}
slices.Sort(matrixMentions.UserIDs)
matrixMentions.UserIDs = slices.Compact(matrixMentions.UserIDs)
if msg.MentionEveryone {
matrixMentions.Room = true
}
return &matrixMentions
} }
func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage { func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage {
@@ -534,7 +659,7 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
var htmlParts []string var htmlParts []string
if msg.Interaction != nil { if msg.Interaction != nil {
puppet := portal.bridge.GetPuppetByID(msg.Interaction.User.ID) puppet := portal.bridge.GetPuppetByID(msg.Interaction.User.ID)
puppet.UpdateInfo(nil, msg.Interaction.User) puppet.UpdateInfo(nil, msg.Interaction.User, "")
htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, puppet.MXID, puppet.Name, msg.Interaction.Name)) htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, puppet.MXID, puppet.Name, msg.Interaction.Name))
} }
if msg.Content != "" && !isPlainGifMessage(msg) { if msg.Content != "" && !isPlainGifMessage(msg) {
@@ -548,7 +673,7 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
with := log.With(). with := log.With().
Str("embed_type", string(embed.Type)). Str("embed_type", string(embed.Type)).
Int("embed_index", i) Int("embed_index", i)
switch getEmbedType(embed) { switch getEmbedType(msg, embed) {
case EmbedRich: case EmbedRich:
log := with.Str("computed_embed_type", "rich").Logger() log := with.Str("computed_embed_type", "rich").Logger()
htmlParts = append(htmlParts, portal.convertDiscordRichEmbed(log.WithContext(ctx), intent, embed, msg.ID, i)) htmlParts = append(htmlParts, portal.convertDiscordRichEmbed(log.WithContext(ctx), intent, embed, msg.ID, i))
@@ -581,5 +706,11 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
"com.beeper.linkpreviews": previews, "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("<strong>%s</strong>: %s", html.EscapeString(msg.Author.Username), content.FormattedBody)
}
return &ConvertedMessage{Type: event.EventMessage, Content: &content, Extra: extraContent} return &ConvertedMessage{Type: event.EventMessage, Content: &content, Extra: extraContent}
} }

View File

@@ -158,18 +158,6 @@ func (br *DiscordBridge) FormatPuppetMXID(did string) id.UserID {
) )
} }
func (br *DiscordBridge) updatePuppetsContactInfo() {
if br.Config.Homeserver.Software != bridgeconfig.SoftwareHungry {
return
}
for _, puppet := range br.GetAllPuppets() {
if !puppet.ContactInfoSet && puppet.NameSet {
puppet.ResendContactInfo()
puppet.Update()
}
}
}
func (puppet *Puppet) GetDisplayname() string { func (puppet *Puppet) GetDisplayname() string {
return puppet.Name return puppet.Name
} }
@@ -207,7 +195,7 @@ func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) {
} }
func (puppet *Puppet) UpdateName(info *discordgo.User) bool { 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 { if puppet.Name == newName && puppet.NameSet {
return false return false
} }
@@ -228,30 +216,54 @@ func (puppet *Puppet) UpdateName(info *discordgo.User) bool {
return true return true
} }
func (br *DiscordBridge) reuploadUserAvatar(intent *appservice.IntentAPI, guildID, userID, avatarID string) (id.ContentURI, error) {
var downloadURL, ext string
if guildID == "" {
downloadURL = discordgo.EndpointUserAvatar(userID, avatarID)
ext = "png"
if strings.HasPrefix(avatarID, "a_") {
downloadURL = discordgo.EndpointUserAvatarAnimated(userID, avatarID)
ext = "gif"
}
} else {
downloadURL = discordgo.EndpointGuildMemberAvatar(guildID, userID, avatarID)
ext = "png"
if strings.HasPrefix(avatarID, "a_") {
downloadURL = discordgo.EndpointGuildMemberAvatarAnimated(guildID, userID, avatarID)
ext = "gif"
}
}
url := br.Config.Bridge.MediaPatterns.Avatar(userID, avatarID, ext)
if !url.IsEmpty() {
return url, nil
}
copied, err := br.copyAttachmentToMatrix(intent, downloadURL, false, AttachmentMeta{
AttachmentID: fmt.Sprintf("avatar/%s/%s/%s", guildID, userID, avatarID),
})
if err != nil {
return url, err
}
return copied.MXC, nil
}
func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool { func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
if puppet.Avatar == info.Avatar && puppet.AvatarSet { avatarID := info.Avatar
if puppet.IsWebhook && !puppet.bridge.Config.Bridge.EnableWebhookAvatars {
avatarID = ""
}
if puppet.Avatar == avatarID && puppet.AvatarSet {
return false return false
} }
avatarChanged := info.Avatar != puppet.Avatar avatarChanged := avatarID != puppet.Avatar
puppet.Avatar = info.Avatar puppet.Avatar = avatarID
puppet.AvatarSet = false puppet.AvatarSet = false
puppet.AvatarURL = id.ContentURI{} puppet.AvatarURL = id.ContentURI{}
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) { if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
downloadURL := discordgo.EndpointUserAvatar(info.ID, info.Avatar) url, err := puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), "", info.ID, puppet.Avatar)
ext := "png" if err != nil {
if strings.HasPrefix(info.Avatar, "a_") { puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
downloadURL = discordgo.EndpointUserAvatarAnimated(info.ID, info.Avatar) return true
ext = "gif"
}
url := puppet.bridge.Config.Bridge.MediaPatterns.Avatar(info.ID, info.Avatar, ext)
if url.IsEmpty() {
var err error
url, err = uploadAvatar(puppet.DefaultIntent(), downloadURL)
if err != nil {
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
return true
}
} }
puppet.AvatarURL = url puppet.AvatarURL = url
} }
@@ -271,7 +283,7 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
return true return true
} }
func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User) { func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User, webhookID string) {
puppet.syncLock.Lock() puppet.syncLock.Lock()
defer puppet.syncLock.Unlock() defer puppet.syncLock.Unlock()
@@ -294,6 +306,10 @@ func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User) {
} }
changed := false changed := false
if webhookID != "" && webhookID == info.ID && !puppet.IsWebhook {
puppet.IsWebhook = true
changed = true
}
changed = puppet.UpdateContactInfo(info) || changed changed = puppet.UpdateContactInfo(info) || changed
changed = puppet.UpdateName(info) || changed changed = puppet.UpdateName(info) || changed
changed = puppet.UpdateAvatar(info) || changed changed = puppet.UpdateAvatar(info) || changed
@@ -308,6 +324,10 @@ func (puppet *Puppet) UpdateContactInfo(info *discordgo.User) bool {
puppet.Username = info.Username puppet.Username = info.Username
changed = true changed = true
} }
if puppet.GlobalName != info.GlobalName {
puppet.GlobalName = info.GlobalName
changed = true
}
if puppet.Discriminator != info.Discriminator { if puppet.Discriminator != info.Discriminator {
puppet.Discriminator = info.Discriminator puppet.Discriminator = info.Discriminator
changed = true changed = true
@@ -316,7 +336,7 @@ func (puppet *Puppet) UpdateContactInfo(info *discordgo.User) bool {
puppet.IsBot = info.Bot puppet.IsBot = info.Bot
changed = true changed = true
} }
if changed { if (changed && !puppet.IsWebhook) || !puppet.ContactInfoSet {
puppet.ContactInfoSet = false puppet.ContactInfoSet = false
puppet.ResendContactInfo() puppet.ResendContactInfo()
return true return true
@@ -337,6 +357,9 @@ func (puppet *Puppet) ResendContactInfo() {
"com.beeper.bridge.network": puppet.bridge.BeeperNetworkName, "com.beeper.bridge.network": puppet.bridge.BeeperNetworkName,
"com.beeper.bridge.is_network_bot": puppet.IsBot, "com.beeper.bridge.is_network_bot": puppet.IsBot,
} }
if puppet.IsWebhook {
contactInfo["com.beeper.bridge.identifiers"] = []string{}
}
err := puppet.DefaultIntent().BeeperUpdateProfile(contactInfo) err := puppet.DefaultIntent().BeeperUpdateProfile(contactInfo)
if err != nil { if err != nil {
puppet.log.Warn().Err(err).Msg("Failed to store custom contact info in profile") puppet.log.Warn().Err(err).Msg("Failed to store custom contact info in profile")

15
user.go
View File

@@ -186,6 +186,12 @@ func (br *DiscordBridge) GetUserByID(id string) *User {
return user return user
} }
func (br *DiscordBridge) GetCachedUserByID(id string) *User {
br.usersLock.Lock()
defer br.usersLock.Unlock()
return br.usersByID[id]
}
func (br *DiscordBridge) NewUser(dbUser *database.User) *User { func (br *DiscordBridge) NewUser(dbUser *database.User) *User {
user := &User{ user := &User{
User: dbUser, User: dbUser,
@@ -630,6 +636,8 @@ func (user *User) eventHandler(rawEvt any) {
user.pushPortalMessage(evt, "message create", evt.ChannelID, evt.GuildID) user.pushPortalMessage(evt, "message create", evt.ChannelID, evt.GuildID)
case *discordgo.MessageDelete: case *discordgo.MessageDelete:
user.pushPortalMessage(evt, "message delete", evt.ChannelID, evt.GuildID) user.pushPortalMessage(evt, "message delete", evt.ChannelID, evt.GuildID)
case *discordgo.MessageDeleteBulk:
user.pushPortalMessage(evt, "bulk message delete", evt.ChannelID, evt.GuildID)
case *discordgo.MessageUpdate: case *discordgo.MessageUpdate:
user.pushPortalMessage(evt, "message update", evt.ChannelID, evt.GuildID) user.pushPortalMessage(evt, "message update", evt.ChannelID, evt.GuildID)
case *discordgo.MessageReactionAdd: case *discordgo.MessageReactionAdd:
@@ -1226,10 +1234,17 @@ func (user *User) messageAckHandler(m *discordgo.MessageAck) {
} }
func (user *User) typingStartHandler(t *discordgo.TypingStart) { func (user *User) typingStartHandler(t *discordgo.TypingStart) {
if t.UserID == user.DiscordID {
return
}
portal := user.GetExistingPortalByID(t.ChannelID) portal := user.GetExistingPortalByID(t.ChannelID)
if portal == nil || portal.MXID == "" { if portal == nil || portal.MXID == "" {
return return
} }
targetUser := user.bridge.GetCachedUserByID(t.UserID)
if targetUser != nil {
return
}
portal.handleDiscordTyping(t) portal.handleDiscordTyping(t)
} }