41 Commits

Author SHA1 Message Date
Tulir Asokan
a5f9d6510b Bump version to v0.4.0 2023-05-16 17:53:02 +03:00
Tulir Asokan
cf7ae7c4db Ignore updates to outgoing webhook messages 2023-05-15 20:00:15 +03:00
Tulir Asokan
ad8efb864b Add option to disable direct CDN uploads 2023-05-14 14:46:04 +03:00
Tulir Asokan
de80a77708 Update mautrix-go 2023-05-12 00:56:18 +03:00
Tulir Asokan
1ca06f7731 Include discord style identifier with timestamps 2023-05-10 15:25:57 +03:00
Tulir Asokan
d3613d1ec0 Use helper methods for generating matrix.to URLs 2023-05-10 15:25:57 +03:00
Tulir Asokan
6f4c5c1d77 Fix bridging animated emojis in messages
Fixes #87
2023-05-10 15:25:57 +03:00
Tulir Asokan
d3b6c3bc9f Update mautrix-go 2023-05-10 15:25:57 +03:00
vurpo
7655ff1a64 Set contact info for puppets on startup (#85) 2023-05-08 16:58:30 +03:00
Tulir Asokan
87c90d3f12 Maybe fix db upgrade for sqlites in weird states 2023-05-06 23:08:29 +03:00
Tulir Asokan
8100386f88 Set times to utc when reading from database 2023-05-06 22:59:32 +03:00
Tulir Asokan
102b1510f8 Bridge incoming reply embeds as replies 2023-05-06 22:59:23 +03:00
Tulir Asokan
4324b60a2c Store edit timestamp in database to deduplicate edits. Fixes #86 2023-05-06 22:23:19 +03:00
Tulir Asokan
c26de9c7df Update changelog 2023-05-06 21:44:07 +03:00
Tulir Asokan
2937c3ea2e Add new field to reactions 2023-05-06 21:43:57 +03:00
Tulir Asokan
6738a04715 Move zerolog.CallerMarshalFunc to mautrix-go 2023-05-06 20:18:13 +03:00
Tulir Asokan
35f534affa Use convert replies to embeds when sending via webhook
Fixes #68
2023-05-06 18:48:53 +03:00
Tulir Asokan
2e07cbfa0b Update dependencies 2023-05-06 17:45:56 +03:00
Tulir Asokan
cc2d0ae40d Add options to disable or force-enable caching media 2023-05-05 12:51:12 +03:00
Tulir Asokan
9793e00434 Fix message on captcha errors 2023-05-03 18:44:51 +03:00
Tulir Asokan
bd56d33c89 Convert Portal to zerolog 2023-04-30 18:50:30 +03:00
Tulir Asokan
a44ceea836 Fix some unused parameters 2023-04-28 16:06:20 +03:00
Tulir Asokan
f6c4f49bb0 Ensure user invited when updating portal info. Probably fixes #62 2023-04-28 14:58:24 +03:00
Tulir Asokan
14c6ae8c75 Set db compat version 2023-04-28 14:50:47 +03:00
Tulir Asokan
568e270540 Receive all events in same function 2023-04-26 22:04:29 +03:00
Tulir Asokan
3e1d1740f7 Sync group DM participants on change 2023-04-26 21:19:06 +03:00
Tulir Asokan
0e5faa5510 Store username/discriminator/bot status in puppet table 2023-04-26 21:18:46 +03:00
Tulir Asokan
f6f6ed29ec Add option to bypass homeserver for Discord media 2023-04-26 01:39:17 +03:00
Tulir Asokan
f247c679de Add user ID to discordgo logs 2023-04-25 20:48:06 +03:00
Tulir Asokan
aea88ad68f Update mautrix-go 2023-04-25 20:38:38 +03:00
Tulir Asokan
7b93d9099d Enable discordgo info logs by default 2023-04-25 20:33:47 +03:00
Tulir Asokan
3f3c86754d Bridge friend nicks as DM room name 2023-04-22 02:50:14 +03:00
Tulir Asokan
049ef48fb0 Make error messages cleaner 2023-04-22 01:44:51 +03:00
Tulir Asokan
29e0b9fa02 Merge pull request #81 from odrling/backfill-collect-fix
Fix backfill only collecting the last 50 messages
2023-04-20 21:15:06 +03:00
odrling
f298230dcf Fix backfill only collecting the last 50 messages 2023-04-20 19:09:41 +02:00
Tulir Asokan
e3ff8d2269 Sort private channel list before syncing 2023-04-20 14:27:37 +03:00
Tulir Asokan
3df81f40d5 Fix is_network_bot flag name and omit is_bridge_bot 2023-04-18 19:14:38 +03:00
Tulir Asokan
f0bab64e5b Unsplit fetching user info from Puppet.UpdateInfo 2023-04-18 18:43:32 +03:00
Tulir Asokan
1048a41c48 Split converting batch messages into separate function 2023-04-18 18:40:45 +03:00
Sumner Evans
e7f73c3ae2 puppet: update contact info as part of member event changes
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-18 09:14:30 -06:00
Sumner Evans
7469b2577d db/puppet: add contact_info_set column
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-17 21:56:05 -06:00
28 changed files with 1327 additions and 485 deletions

View File

@@ -1,3 +1,23 @@
# v0.4.0 (2023-05-16)
* Added bridging of friend nicks into DM room names.
* Added option to bypass homeserver for Discord media.
See [docs](https://docs.mau.fi/bridges/go/discord/direct-media.html) for more info.
* Added conversion of replies to embeds when sending messages via webhook.
* 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.
* Improved formatting of error messages returned by Discord.
* Enabled discordgo info logs by default.
* Fixed limited backfill always stopping after 50 messages
(thanks to [@odrling] in [#81]).
* Fixed startup sync to sync most recent private channels first.
* Fixed syncing group DM participants when they change.
* Possibly fixed inviting to portal rooms when multiple Matrix users use the
bridge.
[@odrling]: https://github.com/odrling
[#81]: https://github.com/mautrix/discord/pull/81
# v0.3.0 (2023-04-16) # v0.3.0 (2023-04-16)
* Added support for backfilling on room creation and missed messages on startup. * Added support for backfilling on room creation and missed messages on startup.

View File

@@ -246,7 +246,7 @@ func (br *DiscordBridge) convertLottie(data []byte) ([]byte, string, error) {
} }
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta AttachmentMeta) (returnDBFile *database.File, returnErr error) { func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta AttachmentMeta) (returnDBFile *database.File, returnErr error) {
isCacheable := !encrypt isCacheable := br.Config.Bridge.CacheMedia != "never" && (br.Config.Bridge.CacheMedia == "always" || !encrypt)
returnDBFile = br.DB.File.Get(url, encrypt) returnDBFile = br.DB.File.Get(url, encrypt)
if returnDBFile == nil { if returnDBFile == nil {
transferKey := attachmentKey{url, encrypt} transferKey := attachmentKey{url, encrypt}
@@ -288,13 +288,19 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
} }
func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI { func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
var url, mimeType string var url, mimeType, ext string
if animated { if animated {
url = discordgo.EndpointEmojiAnimated(emojiID) url = discordgo.EndpointEmojiAnimated(emojiID)
mimeType = "image/gif" mimeType = "image/gif"
ext = "gif"
} else { } else {
url = discordgo.EndpointEmoji(emojiID) url = discordgo.EndpointEmoji(emojiID)
mimeType = "image/png" mimeType = "image/png"
ext = "png"
}
mxc := portal.bridge.Config.Bridge.MediaPatterns.Emoji(emojiID, ext)
if !mxc.IsEmpty() {
return mxc
} }
dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, AttachmentMeta{ dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, AttachmentMeta{
AttachmentID: emojiID, AttachmentID: emojiID,
@@ -302,7 +308,7 @@ func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool
EmojiName: name, EmojiName: name,
}) })
if err != nil { if err != nil {
portal.log.Warnfln("Failed to download emoji %s from discord: %v", emojiID, err) portal.log.Warn().Err(err).Str("emoji_id", emojiID).Msg("Failed to copy emoji to Matrix")
return id.ContentURI{} return id.ContentURI{}
} }
return dbFile.MXC return dbFile.MXC

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
@@ -31,7 +32,7 @@ func (portal *Portal) forwardBackfillInitial(source *User) {
return return
} }
log := portal.zlog.With(). log := portal.log.With().
Str("action", "initial backfill"). Str("action", "initial backfill").
Str("room_id", portal.MXID.String()). Str("room_id", portal.MXID.String()).
Int("limit", limit). Int("limit", limit).
@@ -52,7 +53,7 @@ func (portal *Portal) ForwardBackfillMissed(source *User, meta *discordgo.Channe
if limit == 0 { if limit == 0 {
return return
} }
log := portal.zlog.With(). log := portal.log.With().
Str("action", "missed event backfill"). Str("action", "missed event backfill").
Str("room_id", portal.MXID.String()). Str("room_id", portal.MXID.String()).
Int("limit", limit). Int("limit", limit).
@@ -110,7 +111,7 @@ func (portal *Portal) collectBackfillMessages(log zerolog.Logger, source *User,
} }
messages = append(messages, newMessages...) messages = append(messages, newMessages...)
log.Debug().Int("count", len(newMessages)).Msg("Added messages to backfill collection") log.Debug().Int("count", len(newMessages)).Msg("Added messages to backfill collection")
if len(newMessages) <= messageFetchChunkSize || len(messages) >= limit { if len(newMessages) < messageFetchChunkSize || len(messages) >= limit {
break break
} }
before = newMessages[len(newMessages)-1].ID before = newMessages[len(newMessages)-1].ID
@@ -182,8 +183,31 @@ func (portal *Portal) sendBackfillBatch(log zerolog.Logger, source *User, messag
} }
func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, messages []*discordgo.Message) { func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, messages []*discordgo.Message) {
evts, dbMessages := portal.convertMessageBatch(log, source, messages)
if len(evts) == 0 {
log.Warn().Msg("Didn't get any events to backfill")
return
}
log.Info().Int("events", len(evts)).Msg("Converted messages to backfill")
resp, err := portal.MainIntent().BatchSend(portal.MXID, &mautrix.ReqBatchSend{
BeeperNewMessages: true,
Events: evts,
})
if err != nil {
log.Err(err).Msg("Error sending backfill batch")
return
}
for i, evtID := range resp.EventIDs {
dbMessages[i].MXID = evtID
}
portal.bridge.DB.Message.MassInsert(portal.Key, dbMessages)
log.Info().Msg("Inserted backfilled batch to database")
}
func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, messages []*discordgo.Message) ([]*event.Event, []database.Message) {
evts := make([]*event.Event, 0, len(messages)) evts := make([]*event.Event, 0, len(messages))
dbMessages := make([]database.Message, 0, len(messages)) dbMessages := make([]database.Message, 0, len(messages))
ctx := context.Background()
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)
@@ -193,10 +217,15 @@ func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, message
puppet := portal.bridge.GetPuppetByID(msg.Author.ID) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
puppet.UpdateInfo(source, msg.Author) puppet.UpdateInfo(source, msg.Author)
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
replyTo := portal.getReplyTarget(source, msg.MessageReference, true) replyTo := portal.getReplyTarget(source, "", msg.MessageReference, msg.Embeds, true)
ts, _ := discordgo.SnowflakeTimestamp(msg.ID) ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
parts := portal.convertDiscordMessage(intent, msg) log := log.With().
Str("message_id", msg.ID).
Int("message_type", int(msg.Type)).
Str("author_id", msg.Author.ID).
Logger()
parts := portal.convertDiscordMessage(log.WithContext(ctx), 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}
@@ -236,24 +265,7 @@ func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, message
}) })
} }
} }
if len(evts) == 0 { return evts, dbMessages
log.Warn().Msg("Didn't get any events to backfill")
return
}
log.Info().Int("events", len(evts)).Msg("Converted messages to backfill")
resp, err := portal.MainIntent().BatchSend(portal.MXID, &mautrix.ReqBatchSend{
BeeperNewMessages: true,
Events: evts,
})
if err != nil {
log.Err(err).Msg("Error sending backfill batch")
return
}
for i, evtID := range resp.EventIDs {
dbMessages[i].MXID = evtID
}
portal.bridge.DB.Message.MassInsert(portal.Key, dbMessages)
log.Info().Msg("Inserted backfilled batch to database")
} }
func (portal *Portal) deterministicEventID(messageID, partName string) id.EventID { func (portal *Portal) deterministicEventID(messageID, partName string) id.EventID {

View File

@@ -371,10 +371,10 @@ func fnRejoinSpace(ce *WrappedCommandEvent) {
} }
user := ce.User user := ce.User
if ce.Args[0] == "main" { if ce.Args[0] == "main" {
user.ensureInvited(nil, user.GetSpaceRoom(), false) user.ensureInvited(nil, user.GetSpaceRoom(), false, true)
ce.Reply("Invited you to your main space ([link](%s))", user.GetSpaceRoom().URI(ce.Bridge.AS.HomeserverDomain).MatrixToURL()) ce.Reply("Invited you to your main space ([link](%s))", user.GetSpaceRoom().URI(ce.Bridge.AS.HomeserverDomain).MatrixToURL())
} else if ce.Args[0] == "dms" { } else if ce.Args[0] == "dms" {
user.ensureInvited(nil, user.GetDMSpaceRoom(), false) user.ensureInvited(nil, user.GetDMSpaceRoom(), false, true)
ce.Reply("Invited you to your DM space ([link](%s))", user.GetDMSpaceRoom().URI(ce.Bridge.AS.HomeserverDomain).MatrixToURL()) ce.Reply("Invited you to your DM space ([link](%s))", user.GetDMSpaceRoom().URI(ce.Bridge.AS.HomeserverDomain).MatrixToURL())
} else if _, err := strconv.Atoi(ce.Args[0]); err == nil { } else if _, err := strconv.Atoi(ce.Args[0]); err == nil {
ce.Reply("Rejoining guild spaces is not yet implemented") ce.Reply("Rejoining guild spaces is not yet implemented")

View File

@@ -25,6 +25,7 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"maunium.net/go/mautrix/bridge/bridgeconfig" "maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/id"
) )
type BridgeConfig struct { type BridgeConfig struct {
@@ -50,7 +51,12 @@ 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"`
AnimatedSticker struct { UseDiscordCDNUpload bool `yaml:"use_discord_cdn_upload"`
CacheMedia string `yaml:"cache_media"`
MediaPatterns MediaPatterns `yaml:"media_patterns"`
AnimatedSticker struct {
Target string `yaml:"target"` Target string `yaml:"target"`
Args struct { Args struct {
Width int `yaml:"width"` Width int `yaml:"width"`
@@ -89,6 +95,113 @@ type BridgeConfig struct {
guildNameTemplate *template.Template `yaml:"-"` guildNameTemplate *template.Template `yaml:"-"`
} }
type MediaPatterns struct {
Enabled bool `yaml:"enabled"`
TplAttachments string `yaml:"attachments"`
TplEmojis string `yaml:"emojis"`
TplStickers string `yaml:"stickers"`
TplAvatars string `yaml:"avatars"`
attachments *template.Template `yaml:"-"`
emojis *template.Template `yaml:"-"`
stickers *template.Template `yaml:"-"`
avatars *template.Template `yaml:"-"`
}
type umMediaPatterns MediaPatterns
func (mp *MediaPatterns) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := unmarshal((*umMediaPatterns)(mp))
if err != nil {
return err
}
tpl := template.New("media_patterns")
pairs := []struct {
ptr **template.Template
name string
template string
}{
{&mp.attachments, "attachments", mp.TplAttachments},
{&mp.emojis, "emojis", mp.TplEmojis},
{&mp.stickers, "stickers", mp.TplStickers},
{&mp.avatars, "avatars", mp.TplAvatars},
}
for _, pair := range pairs {
if pair.template == "" {
continue
}
*pair.ptr, err = tpl.New(pair.name).Parse(pair.template)
if err != nil {
return err
}
}
return nil
}
type attachmentParams struct {
ChannelID string
AttachmentID string
FileName string
}
type emojiStickerParams struct {
ID string
Ext string
}
type avatarParams struct {
UserID string
AvatarID string
Ext string
}
func (mp *MediaPatterns) execute(tpl *template.Template, params any) id.ContentURI {
if tpl == nil || !mp.Enabled {
return id.ContentURI{}
}
var out strings.Builder
err := tpl.Execute(&out, params)
if err != nil {
panic(err)
}
uri, err := id.ParseContentURI(out.String())
if err != nil {
panic(err)
}
return uri
}
func (mp *MediaPatterns) Attachment(channelID, attachmentID, filename string) id.ContentURI {
return mp.execute(mp.attachments, attachmentParams{
ChannelID: channelID,
AttachmentID: attachmentID,
FileName: filename,
})
}
func (mp *MediaPatterns) Emoji(emojiID, ext string) id.ContentURI {
return mp.execute(mp.emojis, emojiStickerParams{
ID: emojiID,
Ext: ext,
})
}
func (mp *MediaPatterns) Sticker(stickerID, ext string) id.ContentURI {
return mp.execute(mp.stickers, emojiStickerParams{
ID: stickerID,
Ext: ext,
})
}
func (mp *MediaPatterns) Avatar(userID, avatarID, ext string) id.ContentURI {
return mp.execute(mp.avatars, avatarParams{
UserID: userID,
AvatarID: avatarID,
Ext: ext,
})
}
type BackfillLimitPart struct { type BackfillLimitPart struct {
DM int `yaml:"dm"` DM int `yaml:"dm"`
Channel int `yaml:"channel"` Channel int `yaml:"channel"`

View File

@@ -55,6 +55,13 @@ 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", "use_discord_cdn_upload")
helper.Copy(up.Bool, "bridge", "media_patterns", "enabled")
helper.Copy(up.Str, "bridge", "cache_media")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "attachments")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "emojis")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "stickers")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "avatars")
helper.Copy(up.Str, "bridge", "animated_sticker", "target") helper.Copy(up.Str, "bridge", "animated_sticker", "target")
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "width") helper.Copy(up.Int, "bridge", "animated_sticker", "args", "width")
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "height") helper.Copy(up.Int, "bridge", "animated_sticker", "args", "height")

View File

@@ -68,9 +68,10 @@ func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
return db return db
} }
func strPtr(val string) *string { func strPtr[T ~string](val T) *string {
if val == "" { if val == "" {
return nil return nil
} }
return &val valStr := string(val)
return &valStr
} }

View File

@@ -79,7 +79,7 @@ func (f *File) Scan(row dbutil.Scannable) *File {
} }
f.ID = fileID.String f.ID = fileID.String
f.EmojiName = emojiName.String f.EmojiName = emojiName.String
f.Timestamp = time.UnixMilli(timestamp) f.Timestamp = time.UnixMilli(timestamp).UTC()
f.Width = int(width.Int32) f.Width = int(width.Int32)
f.Height = int(height.Int32) f.Height = int(height.Int32)
f.MXC, err = id.ParseContentURI(mxc) f.MXC, err = id.ParseContentURI(mxc)

View File

@@ -19,7 +19,7 @@ type MessageQuery struct {
} }
const ( const (
messageSelect = "SELECT dcid, dc_attachment_id, dc_edit_index, dc_chan_id, dc_chan_receiver, dc_sender, 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 FROM message"
) )
func (mq *MessageQuery) New() *Message { func (mq *MessageQuery) New() *Message {
@@ -46,17 +46,17 @@ func (mq *MessageQuery) scanAll(rows dbutil.Rows, err error) []*Message {
} }
func (mq *MessageQuery) GetByDiscordID(key PortalKey, discordID string) []*Message { func (mq *MessageQuery) GetByDiscordID(key PortalKey, discordID string) []*Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 AND dc_edit_index=0 ORDER BY dc_attachment_id ASC" query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id ASC"
return mq.scanAll(mq.db.Query(query, key.ChannelID, key.Receiver, discordID)) return mq.scanAll(mq.db.Query(query, key.ChannelID, key.Receiver, discordID))
} }
func (mq *MessageQuery) GetFirstByDiscordID(key PortalKey, discordID string) *Message { func (mq *MessageQuery) GetFirstByDiscordID(key PortalKey, discordID string) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 AND dc_edit_index=0 ORDER BY dc_attachment_id ASC LIMIT 1" query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id ASC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID)) return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID))
} }
func (mq *MessageQuery) GetLastByDiscordID(key PortalKey, discordID string) *Message { func (mq *MessageQuery) GetLastByDiscordID(key PortalKey, discordID string) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 AND dc_edit_index=0 ORDER BY dc_attachment_id DESC LIMIT 1" query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID)) return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID))
} }
@@ -66,12 +66,12 @@ func (mq *MessageQuery) GetClosestBefore(key PortalKey, threadID string, ts time
} }
func (mq *MessageQuery) GetLastInThread(key PortalKey, threadID string) *Message { func (mq *MessageQuery) GetLastInThread(key PortalKey, threadID string) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 AND dc_edit_index=0 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1" query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, threadID)) return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, threadID))
} }
func (mq *MessageQuery) GetLast(key PortalKey) *Message { func (mq *MessageQuery) GetLast(key PortalKey) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_edit_index=0 ORDER BY timestamp DESC LIMIT 1" query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 ORDER BY timestamp DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver)) return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver))
} }
@@ -99,7 +99,7 @@ func (mq *MessageQuery) MassInsert(key PortalKey, msgs []Message) {
if len(msgs) == 0 { if len(msgs) == 0 {
return return
} }
valueStringFormat := "($%d, $%d, $%d, $1, $2, $%d, $%d, $%d, $%d)" valueStringFormat := "($%d, $%d, $1, $2, $%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, "$", "?")
} }
@@ -111,9 +111,9 @@ func (mq *MessageQuery) MassInsert(key PortalKey, msgs []Message) {
baseIndex := 2 + i*7 baseIndex := 2 + i*7
params[baseIndex] = msg.DiscordID params[baseIndex] = msg.DiscordID
params[baseIndex+1] = msg.AttachmentID params[baseIndex+1] = msg.AttachmentID
params[baseIndex+2] = msg.EditIndex params[baseIndex+2] = msg.SenderID
params[baseIndex+3] = msg.SenderID params[baseIndex+3] = msg.Timestamp.UnixMilli()
params[baseIndex+4] = msg.Timestamp.UnixMilli() 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) placeholders[i] = fmt.Sprintf(valueStringFormat, baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7)
@@ -129,13 +129,13 @@ type Message struct {
db *Database db *Database
log log.Logger log log.Logger
DiscordID string DiscordID string
AttachmentID string AttachmentID string
EditIndex int Channel PortalKey
Channel PortalKey SenderID string
SenderID string Timestamp time.Time
Timestamp time.Time EditTimestamp time.Time
ThreadID string ThreadID string
MXID id.EventID MXID id.EventID
} }
@@ -149,9 +149,9 @@ func (m *Message) DiscordProtoChannelID() string {
} }
func (m *Message) Scan(row dbutil.Scannable) *Message { func (m *Message) Scan(row dbutil.Scannable) *Message {
var ts int64 var ts, editTS int64
err := row.Scan(&m.DiscordID, &m.AttachmentID, &m.EditIndex, &m.Channel.ChannelID, &m.Channel.Receiver, &m.SenderID, &ts, &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)
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)
@@ -162,7 +162,10 @@ func (m *Message) Scan(row dbutil.Scannable) *Message {
} }
if ts != 0 { if ts != 0 {
m.Timestamp = time.UnixMilli(ts) m.Timestamp = time.UnixMilli(ts).UTC()
}
if editTS != 0 {
m.EditTimestamp = time.Unix(0, editTS).UTC()
} }
return m return m
@@ -170,7 +173,7 @@ func (m *Message) Scan(row dbutil.Scannable) *Message {
const messageInsertQuery = ` const messageInsertQuery = `
INSERT INTO message ( INSERT INTO message (
dcid, dc_attachment_id, dc_edit_index, dc_chan_id, dc_chan_receiver, dc_sender, 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
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
` `
@@ -182,6 +185,13 @@ type MessagePart struct {
MXID id.EventID MXID id.EventID
} }
func (m *Message) editTimestampVal() int64 {
if m.EditTimestamp.IsZero() {
return 0
}
return m.EditTimestamp.UnixNano()
}
func (m *Message) MassInsertParts(msgs []MessagePart) { func (m *Message) MassInsertParts(msgs []MessagePart) {
if len(msgs) == 0 { if len(msgs) == 0 {
return return
@@ -193,11 +203,11 @@ func (m *Message) MassInsertParts(msgs []MessagePart) {
params := make([]interface{}, 7+len(msgs)*2) params := make([]interface{}, 7+len(msgs)*2)
placeholders := make([]string, len(msgs)) placeholders := make([]string, len(msgs))
params[0] = m.DiscordID params[0] = m.DiscordID
params[1] = m.EditIndex params[1] = m.Channel.ChannelID
params[2] = m.Channel.ChannelID params[2] = m.Channel.Receiver
params[3] = m.Channel.Receiver params[3] = m.SenderID
params[4] = m.SenderID params[4] = m.Timestamp.UnixMilli()
params[5] = m.Timestamp.UnixMilli() params[5] = m.editTimestampVal()
params[6] = m.ThreadID params[6] = m.ThreadID
for i, msg := range msgs { for i, msg := range msgs {
params[7+i*2] = msg.AttachmentID params[7+i*2] = msg.AttachmentID
@@ -213,8 +223,8 @@ 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.EditIndex, m.Channel.ChannelID, m.Channel.Receiver, m.SenderID, m.DiscordID, m.AttachmentID, m.Channel.ChannelID, m.Channel.Receiver, m.SenderID,
m.Timestamp.UnixMilli(), m.ThreadID, m.MXID) m.Timestamp.UnixMilli(), m.editTimestampVal(), m.ThreadID, m.MXID)
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)
@@ -222,6 +232,20 @@ func (m *Message) Insert() {
} }
} }
const editUpdateQuery = `
UPDATE message
SET dc_edit_timestamp=$1
WHERE dcid=$2 AND dc_attachment_id=$3 AND dc_chan_id=$4 AND dc_chan_receiver=$5 AND dc_edit_timestamp<$1
`
func (m *Message) UpdateEditTimestamp(ts time.Time) {
_, err := m.db.Exec(editUpdateQuery, ts.UnixNano(), m.DiscordID, m.AttachmentID, m.Channel.ChannelID, m.Channel.Receiver)
if err != nil {
m.log.Warnfln("Failed to update edit timestamp of %s@%s: %v", m.DiscordID, m.Channel, err)
panic(err)
}
}
func (m *Message) Delete() { func (m *Message) Delete() {
query := "DELETE FROM message WHERE dcid=$1 AND dc_chan_id=$2 AND dc_chan_receiver=$3 AND dc_attachment_id=$4" query := "DELETE FROM message WHERE dcid=$1 AND dc_chan_id=$2 AND dc_chan_receiver=$3 AND dc_attachment_id=$4"
_, err := m.db.Exec(query, m.DiscordID, m.Channel.ChannelID, m.Channel.Receiver, m.AttachmentID) _, err := m.db.Exec(query, m.DiscordID, m.Channel.ChannelID, m.Channel.Receiver, m.AttachmentID)

View File

@@ -15,7 +15,7 @@ import (
const ( const (
portalSelect = ` portalSelect = `
SELECT dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid, 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, plain_name, name, name_set, friend_nick, topic, topic_set, avatar, avatar_url, avatar_set,
encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret
FROM portal FROM portal
` `
@@ -68,6 +68,10 @@ func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
return pq.get(portalSelect+" WHERE mxid=$1", mxid) return pq.get(portalSelect+" WHERE mxid=$1", mxid)
} }
func (pq *PortalQuery) FindPrivateChatBetween(id, receiver string) *Portal {
return pq.get(portalSelect+" WHERE other_user_id=$1 AND receiver=$2 AND type=$3", id, receiver, discordgo.ChannelTypeDM)
}
func (pq *PortalQuery) FindPrivateChatsWith(id string) []*Portal { func (pq *PortalQuery) FindPrivateChatsWith(id string) []*Portal {
return pq.getAll(portalSelect+" WHERE other_user_id=$1 AND type=$2", id, discordgo.ChannelTypeDM) return pq.getAll(portalSelect+" WHERE other_user_id=$1 AND type=$2", id, discordgo.ChannelTypeDM)
} }
@@ -109,16 +113,17 @@ type Portal struct {
MXID id.RoomID MXID id.RoomID
PlainName string PlainName string
Name string Name string
NameSet bool NameSet bool
Topic string FriendNick bool
TopicSet bool Topic string
Avatar string TopicSet bool
AvatarURL id.ContentURI Avatar string
AvatarSet bool AvatarURL id.ContentURI
Encrypted bool AvatarSet bool
InSpace id.RoomID Encrypted bool
InSpace id.RoomID
FirstEventID id.EventID FirstEventID id.EventID
@@ -132,7 +137,7 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal {
var avatarURL string var avatarURL string
err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &chanType, &otherUserID, &guildID, &parentID, 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, &mxid, &p.PlainName, &p.Name, &p.NameSet, &p.FriendNick, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet,
&p.Encrypted, &p.InSpace, &firstEventID, &relayWebhookID, &relayWebhookSecret) &p.Encrypted, &p.InSpace, &firstEventID, &relayWebhookID, &relayWebhookSecret)
if err != nil { if err != nil {
@@ -160,13 +165,13 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal {
func (p *Portal) Insert() { func (p *Portal) Insert() {
query := ` query := `
INSERT INTO portal (dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid, 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, plain_name, name, name_set, friend_nick, topic, topic_set, avatar, avatar_url, avatar_set,
encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret) 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
` `
_, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.Type, _, 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)), 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.PlainName, p.Name, p.NameSet, p.FriendNick, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet,
p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret)) p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret))
if err != nil { if err != nil {
@@ -179,14 +184,16 @@ func (p *Portal) Update() {
query := ` query := `
UPDATE portal UPDATE portal
SET type=$1, other_user_id=$2, dc_guild_id=$3, dc_parent_id=$4, mxid=$5, 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, plain_name=$6, name=$7, name_set=$8, friend_nick=$9, topic=$10, topic_set=$11,
encrypted=$14, in_space=$15, first_event_id=$16, relay_webhook_id=$17, relay_webhook_secret=$18 avatar=$12, avatar_url=$13, avatar_set=$14, encrypted=$15, in_space=$16, first_event_id=$17,
WHERE dcid=$19 AND receiver=$20 relay_webhook_id=$18, relay_webhook_secret=$19
WHERE dcid=$20 AND receiver=$21
` `
_, err := p.db.Exec(query, _, err := p.db.Exec(query,
p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)), 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.PlainName, p.Name, p.NameSet, p.FriendNick, p.Topic, p.TopicSet,
p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret), p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.Encrypted, p.InSpace, p.FirstEventID.String(),
strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret),
p.Key.ChannelID, p.Key.Receiver) p.Key.ChannelID, p.Key.Receiver)
if err != nil { if err != nil {

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," +
" custom_mxid, access_token, next_batch" + " contact_info_set, username, discriminator, is_bot, custom_mxid, access_token, next_batch" +
" FROM puppet " " FROM puppet "
) )
@@ -73,6 +73,12 @@ type Puppet struct {
AvatarURL id.ContentURI AvatarURL id.ContentURI
AvatarSet bool AvatarSet bool
ContactInfoSet bool
Username string
Discriminator string
IsBot bool
CustomMXID id.UserID CustomMXID id.UserID
AccessToken string AccessToken string
NextBatch string NextBatch string
@@ -82,8 +88,8 @@ func (p *Puppet) Scan(row dbutil.Scannable) *Puppet {
var avatarURL string var avatarURL string
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, err := row.Scan(&p.ID, &p.Name, &p.NameSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.ContactInfoSet,
&customMXID, &accessToken, &nextBatch) &p.Username, &p.Discriminator, &p.IsBot, &customMXID, &accessToken, &nextBatch)
if err != nil { if err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
@@ -104,11 +110,11 @@ 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, custom_mxid, access_token, next_batch) 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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
` `
_, err := p.db.Exec(query, p.ID, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, _, err := p.db.Exec(query, p.ID, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet,
strPtr(string(p.CustomMXID)), strPtr(p.AccessToken), strPtr(p.NextBatch)) p.Username, p.Discriminator, p.IsBot, 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)
@@ -118,12 +124,12 @@ 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, UPDATE puppet SET name=$1, name_set=$2, avatar=$3, avatar_url=$4, avatar_set=$5, contact_info_set=$6,
custom_mxid=$6, access_token=$7, next_batch=$8 username=$7, discriminator=$8, is_bot=$9, custom_mxid=$10, access_token=$11, next_batch=$12
WHERE id=$9 WHERE id=$13
` `
_, err := p.db.Exec(query, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, _, err := p.db.Exec(query, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet,
strPtr(string(p.CustomMXID)), strPtr(p.AccessToken), strPtr(p.NextBatch), p.Username, p.Discriminator, p.IsBot, strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch),
p.ID) p.ID)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
-- v0 -> v15: Latest revision -- v0 -> v19: Latest revision
CREATE TABLE guild ( CREATE TABLE guild (
dcid TEXT PRIMARY KEY, dcid TEXT PRIMARY KEY,
@@ -29,6 +29,7 @@ CREATE TABLE portal (
plain_name TEXT NOT NULL, plain_name TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
name_set BOOLEAN NOT NULL, name_set BOOLEAN NOT NULL,
friend_nick BOOLEAN NOT NULL,
topic TEXT NOT NULL, topic TEXT NOT NULL,
topic_set BOOLEAN NOT NULL, topic_set BOOLEAN NOT NULL,
avatar TEXT NOT NULL, avatar TEXT NOT NULL,
@@ -62,11 +63,17 @@ CREATE TABLE thread (
CREATE TABLE puppet ( CREATE TABLE puppet (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
name_set BOOLEAN NOT NULL, name_set BOOLEAN NOT NULL DEFAULT false,
avatar TEXT NOT NULL, avatar TEXT NOT NULL,
avatar_url TEXT NOT NULL, avatar_url TEXT NOT NULL,
avatar_set BOOLEAN NOT NULL, avatar_set BOOLEAN NOT NULL DEFAULT false,
contact_info_set BOOLEAN NOT NULL DEFAULT false,
username TEXT NOT NULL DEFAULT '',
discriminator TEXT NOT NULL DEFAULT '',
is_bot BOOLEAN NOT NULL DEFAULT false,
custom_mxid TEXT, custom_mxid TEXT,
access_token TEXT, access_token TEXT,
@@ -97,18 +104,18 @@ CREATE TABLE user_portal (
); );
CREATE TABLE message ( CREATE TABLE message (
dcid TEXT, dcid TEXT,
dc_attachment_id TEXT, dc_attachment_id TEXT,
dc_edit_index INTEGER, dc_chan_id TEXT,
dc_chan_id TEXT, dc_chan_receiver TEXT,
dc_chan_receiver TEXT, dc_sender TEXT NOT NULL,
dc_sender TEXT NOT NULL, timestamp BIGINT NOT NULL,
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,
PRIMARY KEY (dcid, dc_attachment_id, dc_edit_index, 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
); );
@@ -120,13 +127,12 @@ CREATE TABLE reaction (
dc_emoji_name TEXT, dc_emoji_name TEXT,
dc_thread_id TEXT NOT NULL, dc_thread_id TEXT NOT NULL,
dc_first_attachment_id TEXT NOT NULL, dc_first_attachment_id TEXT NOT NULL,
_dc_first_edit_index INTEGER NOT NULL DEFAULT 0,
mxid TEXT NOT NULL UNIQUE, mxid TEXT NOT NULL UNIQUE,
PRIMARY KEY (dc_chan_id, dc_chan_receiver, dc_msg_id, dc_sender, dc_emoji_name), PRIMARY KEY (dc_chan_id, dc_chan_receiver, dc_msg_id, dc_sender, dc_emoji_name),
CONSTRAINT reaction_message_fkey FOREIGN KEY (dc_msg_id, dc_first_attachment_id, _dc_first_edit_index, dc_chan_id, dc_chan_receiver) REFERENCES message (dcid, dc_attachment_id, dc_edit_index, dc_chan_id, dc_chan_receiver) ON DELETE CASCADE CONSTRAINT reaction_message_fkey FOREIGN KEY (dc_msg_id, dc_first_attachment_id, dc_chan_id, dc_chan_receiver) REFERENCES message (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver) ON DELETE CASCADE
); );
CREATE TABLE role ( CREATE TABLE role (

View File

@@ -0,0 +1,3 @@
-- v16: Store whether custom contact info has been set for the puppet
ALTER TABLE puppet ADD COLUMN contact_info_set BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,2 @@
-- v17: Store whether DM portal name is a friend nickname
ALTER TABLE portal ADD COLUMN friend_nick BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,4 @@
-- v18 (compatible with v15+): Store additional metadata for ghosts
ALTER TABLE puppet ADD COLUMN username TEXT NOT NULL DEFAULT '';
ALTER TABLE puppet ADD COLUMN discriminator TEXT NOT NULL DEFAULT '';
ALTER TABLE puppet ADD COLUMN is_bot BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,15 @@
-- v19: Replace dc_edit_index with dc_edit_timestamp
-- transaction: off
BEGIN;
ALTER TABLE reaction DROP CONSTRAINT reaction_message_fkey;
ALTER TABLE message DROP CONSTRAINT message_pkey;
ALTER TABLE message DROP COLUMN dc_edit_index;
ALTER TABLE reaction DROP COLUMN _dc_first_edit_index;
ALTER TABLE message ADD PRIMARY KEY (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver);
ALTER TABLE reaction ADD CONSTRAINT reaction_message_fkey FOREIGN KEY (dc_msg_id, dc_first_attachment_id, dc_chan_id, dc_chan_receiver) REFERENCES message (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver) ON DELETE CASCADE;
ALTER TABLE message ADD COLUMN dc_edit_timestamp BIGINT NOT NULL DEFAULT 0;
ALTER TABLE message ALTER COLUMN dc_edit_timestamp DROP DEFAULT;
COMMIT;

View File

@@ -0,0 +1,48 @@
-- v19: Replace dc_edit_index with dc_edit_timestamp
-- transaction: off
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE message_new (
dcid TEXT,
dc_attachment_id TEXT,
dc_chan_id TEXT,
dc_chan_receiver TEXT,
dc_sender TEXT NOT NULL,
timestamp BIGINT NOT NULL,
dc_edit_timestamp BIGINT NOT NULL,
dc_thread_id TEXT NOT NULL,
mxid TEXT NOT NULL UNIQUE,
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
);
INSERT INTO message_new (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, dc_edit_timestamp, dc_thread_id, mxid)
SELECT dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, 0, dc_thread_id, mxid FROM message;
DROP TABLE message;
ALTER TABLE message_new RENAME TO message;
CREATE TABLE reaction_new (
dc_chan_id TEXT,
dc_chan_receiver TEXT,
dc_msg_id TEXT,
dc_sender TEXT,
dc_emoji_name TEXT,
dc_thread_id TEXT NOT NULL,
dc_first_attachment_id TEXT NOT NULL,
mxid TEXT NOT NULL UNIQUE,
PRIMARY KEY (dc_chan_id, dc_chan_receiver, dc_msg_id, dc_sender, dc_emoji_name),
CONSTRAINT reaction_message_fkey FOREIGN KEY (dc_msg_id, dc_first_attachment_id, dc_chan_id, dc_chan_receiver) REFERENCES message (dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver) ON DELETE CASCADE
);
INSERT INTO reaction_new (dc_chan_id, dc_chan_receiver, dc_msg_id, dc_sender, dc_emoji_name, dc_thread_id, dc_first_attachment_id, mxid)
SELECT dc_chan_id, dc_chan_receiver, dc_msg_id, dc_sender, dc_emoji_name, COALESCE(dc_thread_id, ''), dc_first_attachment_id, mxid FROM reaction;
DROP TABLE reaction;
ALTER TABLE reaction_new RENAME TO reaction;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;

View File

@@ -29,7 +29,7 @@ func (up UserPortal) Scan(l log.Logger, row dbutil.Scannable) *UserPortal {
l.Errorln("Error scanning user portal:", err) l.Errorln("Error scanning user portal:", err)
panic(err) panic(err)
} }
up.Timestamp = time.UnixMilli(ts) up.Timestamp = time.UnixMilli(ts).UTC()
return &up return &up
} }

View File

@@ -145,6 +145,28 @@ 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
# 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).
use_discord_cdn_upload: true
# Should mxc uris copied from Discord be cached?
# This can be `never` to never cache, `unencrypted` to only cache unencrypted mxc uris, or `always` to cache everything.
# If you have a media repo that generates non-unique mxc uris, you should set this to never.
cache_media: unencrypted
# Patterns for converting Discord media to custom mxc:// URIs instead of reuploading.
# Each of the patterns can be set to null to disable custom URIs for that type of media.
# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html
media_patterns:
# Should custom mxc:// URIs be used instead of reuploading media?
enabled: false
# Pattern for normal message attachments.
attachments: mxc://discord-media.mau.dev/attachments|{{.ChannelID}}|{{.AttachmentID}}|{{.FileName}}
# Pattern for custom emojis.
emojis: mxc://discord-media.mau.dev/emojis|{{.ID}}.{{.Ext}}
# Pattern for stickers. Note that animated lottie stickers will not be converted if this is enabled.
stickers: mxc://discord-media.mau.dev/stickers|{{.ID}}.{{.Ext}}
# Pattern for static user avatars.
avatars: mxc://discord-media.mau.dev/avatars|{{.UserID}}|{{.AvatarID}}.{{.Ext}}
# Settings for converting animated stickers. # Settings for converting animated stickers.
animated_sticker: animated_sticker:
# Format to which animated stickers should be converted. # Format to which animated stickers should be converted.

View File

@@ -192,7 +192,7 @@ func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.C
case strings.HasPrefix(tagName, ":"): case strings.HasPrefix(tagName, ":"):
return &astDiscordCustomEmoji{name: tagName, astDiscordTag: tag} return &astDiscordCustomEmoji{name: tagName, astDiscordTag: tag}
case strings.HasPrefix(tagName, "a:"): case strings.HasPrefix(tagName, "a:"):
return &astDiscordCustomEmoji{name: tagName[1:], astDiscordTag: tag} return &astDiscordCustomEmoji{name: tagName[1:], astDiscordTag: tag, animated: true}
default: default:
return nil return nil
} }
@@ -263,9 +263,9 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
switch node := n.(type) { switch node := n.(type) {
case *astDiscordUserMention: case *astDiscordUserMention:
if user := node.portal.bridge.GetUserByID(strconv.FormatInt(node.id, 10)); user != nil { if user := node.portal.bridge.GetUserByID(strconv.FormatInt(node.id, 10)); user != nil {
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%[1]s">%[1]s</a>`, user.MXID) _, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, user.MXID.URI().MatrixToURL(), user.MXID)
} else if puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10)); puppet != nil { } else if puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10)); puppet != nil {
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Name) _, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, puppet.MXID.URI().MatrixToURL(), puppet.Name)
} }
return return
case *astDiscordRoleMention: case *astDiscordRoleMention:
@@ -281,7 +281,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
}) })
if portal != nil { if portal != nil {
if portal.MXID != "" { if portal.MXID != "" {
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s?via=%s">%s</a>`, portal.MXID, portal.bridge.AS.HomeserverDomain, portal.Name) _, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, portal.MXID.URI(portal.bridge.AS.HomeserverDomain).MatrixToURL(), portal.Name)
} else { } else {
_, _ = w.WriteString(portal.Name) _, _ = w.WriteString(portal.Name)
} }
@@ -290,7 +290,11 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
case *astDiscordCustomEmoji: case *astDiscordCustomEmoji:
reactionMXC := node.portal.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated) reactionMXC := node.portal.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated)
if !reactionMXC.IsEmpty() { if !reactionMXC.IsEmpty() {
_, _ = fmt.Fprintf(w, `<img data-mx-emoticon src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name) attrs := "data-mx-emoticon"
if node.animated {
attrs += " data-mau-animated-emoji"
}
_, _ = fmt.Fprintf(w, `<img %[3]s src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name, attrs)
return return
} }
case *astDiscordTimestamp: case *astDiscordTimestamp:
@@ -305,7 +309,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
const fullDatetimeFormat = "2006-01-02T15:04:05.000-0700" const fullDatetimeFormat = "2006-01-02T15:04:05.000-0700"
fullRFC := ts.Format(fullDatetimeFormat) fullRFC := ts.Format(fullDatetimeFormat)
fullHumanReadable := ts.Format(discordTimestampStyle('F').Format()) fullHumanReadable := ts.Format(discordTimestampStyle('F').Format())
_, _ = fmt.Fprintf(w, `<time title="%s" datetime="%s"><strong>%s</strong></time>`, fullHumanReadable, fullRFC, formatted) _, _ = fmt.Fprintf(w, `<time title="%s" datetime="%s" data-discord-style="%c"><strong>%s</strong></time>`, fullHumanReadable, fullRFC, node.style, formatted)
} }
stringifiable, ok := n.(fmt.Stringer) stringifiable, ok := n.(fmt.Stringer)
if ok { if ok {

13
go.mod
View File

@@ -8,14 +8,14 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
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.8 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
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.2
github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark v1.5.4
maunium.net/go/maulogger/v2 v2.4.1 maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.15.1 maunium.net/go/mautrix v0.15.2
) )
require ( require (
@@ -29,12 +29,13 @@ 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.8.0 // indirect golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.9.0 // indirect golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/sys v0.7.0 // indirect golang.org/x/net v0.10.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-20230416132336-325ee6a8c961 replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230426184739-79aea97f6660

33
go.sum
View File

@@ -1,6 +1,6 @@
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-20230416132336-325ee6a8c961 h1:eSGaliexlehYBeP4YQW8dQpV9XWWgfR1qH8kfHgrDcY= github.com/beeper/discordgo v0.0.0-20230426184739-79aea97f6660 h1:5LFnUY/Aj/0k/UqeEmW2GS4ql1vxmivkrckPxUHf8oc=
github.com/beeper/discordgo v0.0.0-20230416132336-325ee6a8c961/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/beeper/discordgo v0.0.0-20230426184739-79aea97f6660/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,11 +13,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.8/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
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=
@@ -52,20 +51,16 @@ 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.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
@@ -77,5 +72,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/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.1 h1:pmCtMjYRpd83+2UL+KTRFYQo5to0373yulimvLK+1k0= maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU=
maunium.net/go/mautrix v0.15.1/go.mod h1:icQIrvz2NldkRLTuzSGzmaeuMUmw+fzO7UVycPeauN8= maunium.net/go/mautrix v0.15.2/go.mod h1:h4NwfKqE4YxGTLSgn/gawKzXAb2sF4qx8agL6QEFtGg=

View File

@@ -22,6 +22,7 @@ import (
"sync" "sync"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/maulogger/v2/maulogadapt"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
@@ -219,7 +220,7 @@ func (guild *Guild) CreateMatrixRoom(user *User, meta *discordgo.Guild) error {
guild.bridge.guildsLock.Unlock() guild.bridge.guildsLock.Unlock()
guild.log.Infoln("Matrix room created:", guild.MXID) guild.log.Infoln("Matrix room created:", guild.MXID)
user.ensureInvited(nil, guild.MXID, false) user.ensureInvited(nil, guild.MXID, false, true)
return nil return nil
} }
@@ -236,6 +237,7 @@ func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.G
guild.UpdateBridgeInfo() guild.UpdateBridgeInfo()
guild.Update() guild.Update()
} }
source.ensureInvited(nil, guild.MXID, false, false)
return meta return meta
} }
@@ -295,12 +297,12 @@ func (guild *Guild) cleanup() {
intent := guild.bridge.Bot intent := guild.bridge.Bot
if guild.bridge.SpecVersions.UnstableFeatures["com.beeper.room_yeeting"] { if guild.bridge.SpecVersions.UnstableFeatures["com.beeper.room_yeeting"] {
err := intent.BeeperDeleteRoom(guild.MXID) err := intent.BeeperDeleteRoom(guild.MXID)
if err == nil || errors.Is(err, mautrix.MNotFound) { if err != nil && !errors.Is(err, mautrix.MNotFound) {
return guild.log.Errorfln("Failed to delete %s using hungryserv yeet endpoint: %v", guild.MXID, err)
} }
guild.log.Warnfln("Failed to delete %s using hungryserv yeet endpoint, falling back to normal behavior: %v", guild.MXID, err) return
} }
guild.bridge.cleanupRoom(intent, guild.MXID, false, guild.log) guild.bridge.cleanupRoom(intent, guild.MXID, false, *maulogadapt.MauAsZero(guild.log))
} }
func (guild *Guild) RemoveMXID() { func (guild *Guild) RemoveMXID() {

18
main.go
View File

@@ -18,13 +18,8 @@ package main
import ( import (
_ "embed" _ "embed"
"fmt"
"runtime"
"strings"
"sync" "sync"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/commands" "maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@@ -101,22 +96,13 @@ func (br *DiscordBridge) Init() {
br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database")) br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database"))
discordLog = br.ZLog.With().Str("component", "discordgo").Logger() discordLog = br.ZLog.With().Str("component", "discordgo").Logger()
// TODO move this to mautrix-go?
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
files := strings.Split(file, "/")
file = files[len(files)-1]
name := runtime.FuncForPC(pc).Name()
fns := strings.Split(name, ".")
name = fns[len(fns)-1]
return fmt.Sprintf("%s:%d:%s()", file, line, name)
}
} }
func (br *DiscordBridge) Start() { 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()
go br.startUsers() go br.startUsers()
} }
@@ -190,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.3.0", Version: "0.4.0",
ProtocolName: "Discord", ProtocolName: "Discord",
BeeperServiceName: "discordgo", BeeperServiceName: "discordgo",
BeeperNetworkName: "discord", BeeperNetworkName: "discord",

651
portal.go

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"html" "html"
"strconv" "strconv"
@@ -24,6 +25,7 @@ import (
"time" "time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
@@ -48,14 +50,14 @@ func (portal *Portal) createMediaFailedMessage(bridgeErr error) *event.MessageEv
const DiscordStickerSize = 160 const DiscordStickerSize = 160
func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent) *event.MessageEventContent { func (portal *Portal) convertDiscordFile(ctx context.Context, typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent) *event.MessageEventContent {
meta := AttachmentMeta{AttachmentID: id, MimeType: content.Info.MimeType} meta := AttachmentMeta{AttachmentID: id, MimeType: content.Info.MimeType}
if typeName == "sticker" && content.Info.MimeType == "application/json" { if typeName == "sticker" && content.Info.MimeType == "application/json" {
meta.Converter = portal.bridge.convertLottie meta.Converter = portal.bridge.convertLottie
} }
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, meta) dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, meta)
if err != nil { if err != nil {
portal.log.Errorfln("Error copying attachment %s to Matrix: %v", id, err) zerolog.Ctx(ctx).Err(err).Msg("Failed to copy attachment to Matrix")
return portal.createMediaFailedMessage(err) return portal.createMediaFailedMessage(err)
} }
if typeName == "sticker" && content.Info.MimeType == "application/json" { if typeName == "sticker" && content.Info.MimeType == "application/json" {
@@ -66,10 +68,6 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
content.Info.Width = dbFile.Width content.Info.Width = dbFile.Width
content.Info.Height = dbFile.Height content.Info.Height = dbFile.Height
} }
if content.Info.Width == 0 && content.Info.Height == 0 && typeName == "sticker" {
content.Info.Width = DiscordStickerSize
content.Info.Height = DiscordStickerSize
}
if dbFile.DecryptionInfo != nil { if dbFile.DecryptionInfo != nil {
content.File = &event.EncryptedFileInfo{ content.File = &event.EncryptedFileInfo{
EncryptedFile: *dbFile.DecryptionInfo, EncryptedFile: *dbFile.DecryptionInfo,
@@ -78,8 +76,14 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
} else { } else {
content.URL = dbFile.MXC.CUString() content.URL = dbFile.MXC.CUString()
} }
return content
}
if typeName == "sticker" && (content.Info.Width > DiscordStickerSize || content.Info.Height > DiscordStickerSize) { func (portal *Portal) cleanupConvertedStickerInfo(content *event.MessageEventContent) {
if content.Info.Width == 0 && content.Info.Height == 0 {
content.Info.Width = DiscordStickerSize
content.Info.Height = DiscordStickerSize
} else if content.Info.Width > DiscordStickerSize || content.Info.Height > DiscordStickerSize {
if content.Info.Width > content.Info.Height { if content.Info.Width > content.Info.Height {
content.Info.Height /= content.Info.Width / DiscordStickerSize content.Info.Height /= content.Info.Width / DiscordStickerSize
content.Info.Width = DiscordStickerSize content.Info.Width = DiscordStickerSize
@@ -91,36 +95,51 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
content.Info.Height = DiscordStickerSize content.Info.Height = DiscordStickerSize
} }
} }
return content
} }
func (portal *Portal) convertDiscordSticker(intent *appservice.IntentAPI, sticker *discordgo.Sticker) *ConvertedMessage { func (portal *Portal) convertDiscordSticker(ctx context.Context, intent *appservice.IntentAPI, sticker *discordgo.Sticker) *ConvertedMessage {
var mime string var mime, ext string
switch sticker.FormatType { switch sticker.FormatType {
case discordgo.StickerFormatTypePNG: case discordgo.StickerFormatTypePNG:
mime = "image/png" mime = "image/png"
ext = "png"
case discordgo.StickerFormatTypeAPNG: case discordgo.StickerFormatTypeAPNG:
mime = "image/apng" mime = "image/apng"
ext = "png"
case discordgo.StickerFormatTypeLottie: case discordgo.StickerFormatTypeLottie:
mime = "application/json" mime = "application/json"
ext = "json"
case discordgo.StickerFormatTypeGIF: case discordgo.StickerFormatTypeGIF:
mime = "image/gif" mime = "image/gif"
ext = "gif"
default: default:
portal.log.Warnfln("Unknown sticker format %d in %s", sticker.FormatType, sticker.ID) zerolog.Ctx(ctx).Warn().
Int("sticker_format", int(sticker.FormatType)).
Str("sticker_id", sticker.ID).
Msg("Unknown sticker format")
} }
content := &event.MessageEventContent{
Body: sticker.Name, // TODO find description from somewhere?
Info: &event.FileInfo{
MimeType: mime,
},
}
mxc := portal.bridge.Config.Bridge.MediaPatterns.Sticker(sticker.ID, ext)
if mxc.IsEmpty() {
content = portal.convertDiscordFile(ctx, "sticker", intent, sticker.ID, sticker.URL(), content)
} else {
content.URL = mxc.CUString()
}
portal.cleanupConvertedStickerInfo(content)
return &ConvertedMessage{ return &ConvertedMessage{
AttachmentID: sticker.ID, AttachmentID: sticker.ID,
Type: event.EventSticker, Type: event.EventSticker,
Content: portal.convertDiscordFile("sticker", intent, sticker.ID, sticker.URL(), &event.MessageEventContent{ Content: content,
Body: sticker.Name, // TODO find description from somewhere?
Info: &event.FileInfo{
MimeType: mime,
},
}),
} }
} }
func (portal *Portal) convertDiscordAttachment(intent *appservice.IntentAPI, att *discordgo.MessageAttachment) *ConvertedMessage { func (portal *Portal) convertDiscordAttachment(ctx context.Context, intent *appservice.IntentAPI, att *discordgo.MessageAttachment) *ConvertedMessage {
content := &event.MessageEventContent{ content := &event.MessageEventContent{
Body: att.Filename, Body: att.Filename,
Info: &event.FileInfo{ Info: &event.FileInfo{
@@ -158,7 +177,12 @@ func (portal *Portal) convertDiscordAttachment(intent *appservice.IntentAPI, att
default: default:
content.MsgType = event.MsgFile content.MsgType = event.MsgFile
} }
content = portal.convertDiscordFile("attachment", intent, att.ID, att.URL, content) mxc := portal.bridge.Config.Bridge.MediaPatterns.Attachment(portal.Key.ChannelID, att.ID, att.Filename)
if mxc.IsEmpty() {
content = portal.convertDiscordFile(ctx, "attachment", intent, att.ID, att.URL, content)
} else {
content.URL = mxc.CUString()
}
return &ConvertedMessage{ return &ConvertedMessage{
AttachmentID: att.ID, AttachmentID: att.ID,
Type: event.EventMessage, Type: event.EventMessage,
@@ -167,10 +191,11 @@ func (portal *Portal) convertDiscordAttachment(intent *appservice.IntentAPI, att
} }
} }
func (portal *Portal) convertDiscordVideoEmbed(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) dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, NoMeta)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix")
return &ConvertedMessage{ return &ConvertedMessage{
AttachmentID: attachmentID, AttachmentID: attachmentID,
Type: event.EventMessage, Type: event.EventMessage,
@@ -219,22 +244,24 @@ func (portal *Portal) convertDiscordVideoEmbed(intent *appservice.IntentAPI, emb
} }
} }
func (portal *Portal) convertDiscordMessage(intent *appservice.IntentAPI, msg *discordgo.Message) []*ConvertedMessage { func (portal *Portal) convertDiscordMessage(ctx context.Context, 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++
} }
parts := make([]*ConvertedMessage, 0, predictedLength) parts := make([]*ConvertedMessage, 0, predictedLength)
if textPart := portal.convertDiscordTextMessage(intent, msg); textPart != nil { if textPart := portal.convertDiscordTextMessage(ctx, intent, msg); textPart != nil {
parts = append(parts, textPart) parts = append(parts, textPart)
} }
log := zerolog.Ctx(ctx)
handledIDs := make(map[string]struct{}) handledIDs := make(map[string]struct{})
for _, att := range msg.Attachments { for _, att := range msg.Attachments {
if _, handled := handledIDs[att.ID]; handled { if _, handled := handledIDs[att.ID]; handled {
continue continue
} }
handledIDs[att.ID] = struct{}{} handledIDs[att.ID] = struct{}{}
if part := portal.convertDiscordAttachment(intent, att); part != nil { log := log.With().Str("attachment_id", att.ID).Logger()
if part := portal.convertDiscordAttachment(log.WithContext(ctx), intent, att); part != nil {
parts = append(parts, part) parts = append(parts, part)
} }
} }
@@ -243,11 +270,12 @@ func (portal *Portal) convertDiscordMessage(intent *appservice.IntentAPI, msg *d
continue continue
} }
handledIDs[sticker.ID] = struct{}{} handledIDs[sticker.ID] = struct{}{}
if part := portal.convertDiscordSticker(intent, sticker); part != nil { log := log.With().Str("sticker_id", sticker.ID).Logger()
if part := portal.convertDiscordSticker(log.WithContext(ctx), intent, sticker); part != nil {
parts = append(parts, part) parts = append(parts, part)
} }
} }
for _, 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(embed) != EmbedVideo {
continue continue
@@ -257,7 +285,12 @@ func (portal *Portal) convertDiscordMessage(intent *appservice.IntentAPI, msg *d
continue continue
} }
handledIDs[embed.URL] = struct{}{} handledIDs[embed.URL] = struct{}{}
part := portal.convertDiscordVideoEmbed(intent, embed) log := log.With().
Str("computed_embed_type", "video").
Str("embed_type", string(embed.Type)).
Int("embed_index", i).
Logger()
part := portal.convertDiscordVideoEmbed(log.WithContext(ctx), intent, embed)
if part != nil { if part != nil {
parts = append(parts, part) parts = append(parts, part)
} }
@@ -286,7 +319,8 @@ const (
embedFooterDateSeparator = `` embedFooterDateSeparator = ``
) )
func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed, msgID string, index int) string { func (portal *Portal) convertDiscordRichEmbed(ctx context.Context, intent *appservice.IntentAPI, embed *discordgo.MessageEmbed, msgID string, index int) string {
log := zerolog.Ctx(ctx)
var htmlParts []string var htmlParts []string
if embed.Author != nil { if embed.Author != nil {
var authorHTML string var authorHTML string
@@ -298,7 +332,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
if embed.Author.ProxyIconURL != "" { if embed.Author.ProxyIconURL != "" {
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Author.ProxyIconURL, false, NoMeta) dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Author.ProxyIconURL, false, NoMeta)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to reupload author icon in embed #%d of message %s: %v", index+1, msgID, err) log.Warn().Err(err).Msg("Failed to reupload author icon in embed")
} else { } else {
authorHTML = fmt.Sprintf(embedHTMLAuthorWithImage, dbFile.MXC, authorNameHTML) authorHTML = fmt.Sprintf(embedHTMLAuthorWithImage, dbFile.MXC, authorNameHTML)
} }
@@ -348,7 +382,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
if embed.Image != nil { if embed.Image != nil {
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Image.ProxyURL, false, NoMeta) dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Image.ProxyURL, false, NoMeta)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to reupload image in embed #%d of message %s: %v", index+1, msgID, err) log.Warn().Err(err).Msg("Failed to reupload image in embed")
} else { } else {
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLImage, dbFile.MXC)) htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLImage, dbFile.MXC))
} }
@@ -358,7 +392,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
formattedTime := embed.Timestamp formattedTime := embed.Timestamp
parsedTS, err := time.Parse(time.RFC3339, embed.Timestamp) parsedTS, err := time.Parse(time.RFC3339, embed.Timestamp)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to parse timestamp in embed #%d of message %s: %v", index+1, msgID, err) log.Warn().Err(err).Msg("Failed to parse timestamp in embed")
} else { } else {
formattedTime = parsedTS.Format(discordTimestampStyle('F').Format()) formattedTime = parsedTS.Format(discordTimestampStyle('F').Format())
} }
@@ -374,7 +408,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
if embed.Footer.ProxyIconURL != "" { if embed.Footer.ProxyIconURL != "" {
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Footer.ProxyIconURL, false, NoMeta) dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Footer.ProxyIconURL, false, NoMeta)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to reupload footer icon in embed #%d of message %s: %v", index+1, msgID, err) log.Warn().Err(err).Msg("Failed to reupload footer icon in embed")
} else { } else {
footerHTML = fmt.Sprintf(embedHTMLFooterWithImage, dbFile.MXC, html.EscapeString(embed.Footer.Text), datePart) footerHTML = fmt.Sprintf(embedHTMLFooterWithImage, dbFile.MXC, html.EscapeString(embed.Footer.Text), datePart)
} }
@@ -403,40 +437,40 @@ type BeeperLinkPreview struct {
ImageEncryption *event.EncryptedFileInfo `json:"beeper:image:encryption,omitempty"` ImageEncryption *event.EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
} }
func (portal *Portal) convertDiscordLinkEmbedImage(intent *appservice.IntentAPI, url string, width, height int, preview *BeeperLinkPreview) { func (portal *Portal) convertDiscordLinkEmbedImage(ctx context.Context, intent *appservice.IntentAPI, url string, width, height int, preview *BeeperLinkPreview) {
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, NoMeta) dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, NoMeta)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to copy image in URL preview: %v", err) zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to reupload image in URL preview")
return
}
if width != 0 || height != 0 {
preview.ImageWidth = width
preview.ImageHeight = height
} else { } else {
if width != 0 || height != 0 { preview.ImageWidth = dbFile.Width
preview.ImageWidth = width preview.ImageHeight = dbFile.Height
preview.ImageHeight = height }
} else { preview.ImageSize = dbFile.Size
preview.ImageWidth = dbFile.Width preview.ImageType = dbFile.MimeType
preview.ImageHeight = dbFile.Height if dbFile.Encrypted {
} preview.ImageEncryption = &event.EncryptedFileInfo{
preview.ImageSize = dbFile.Size EncryptedFile: *dbFile.DecryptionInfo,
preview.ImageType = dbFile.MimeType URL: dbFile.MXC.CUString(),
if dbFile.Encrypted {
preview.ImageEncryption = &event.EncryptedFileInfo{
EncryptedFile: *dbFile.DecryptionInfo,
URL: dbFile.MXC.CUString(),
}
} else {
preview.ImageURL = dbFile.MXC.CUString()
} }
} else {
preview.ImageURL = dbFile.MXC.CUString()
} }
} }
func (portal *Portal) convertDiscordLinkEmbedToBeeper(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *BeeperLinkPreview { func (portal *Portal) convertDiscordLinkEmbedToBeeper(ctx context.Context, intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *BeeperLinkPreview {
var preview BeeperLinkPreview var preview BeeperLinkPreview
preview.MatchedURL = embed.URL preview.MatchedURL = embed.URL
preview.Title = embed.Title preview.Title = embed.Title
preview.Description = embed.Description preview.Description = embed.Description
if embed.Image != nil { if embed.Image != nil {
portal.convertDiscordLinkEmbedImage(intent, embed.Image.ProxyURL, embed.Image.Width, embed.Image.Height, &preview) portal.convertDiscordLinkEmbedImage(ctx, intent, embed.Image.ProxyURL, embed.Image.Width, embed.Image.Height, &preview)
} else if embed.Thumbnail != nil { } else if embed.Thumbnail != nil {
portal.convertDiscordLinkEmbedImage(intent, embed.Thumbnail.ProxyURL, embed.Thumbnail.Width, embed.Thumbnail.Height, &preview) portal.convertDiscordLinkEmbedImage(ctx, intent, embed.Thumbnail.ProxyURL, embed.Thumbnail.Width, embed.Thumbnail.Height, &preview)
} }
return &preview return &preview
} }
@@ -484,7 +518,8 @@ 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].Video != nil && msg.Embeds[0].URL == msg.Content && msg.Embeds[0].Type == discordgo.EmbedTypeGifv
} }
func (portal *Portal) convertDiscordTextMessage(intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage { func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage {
log := zerolog.Ctx(ctx)
if msg.Type == discordgo.MessageTypeCall { if msg.Type == discordgo.MessageTypeCall {
return &ConvertedMessage{Type: event.EventMessage, Content: &event.MessageEventContent{ return &ConvertedMessage{Type: event.EventMessage, Content: &event.MessageEventContent{
MsgType: event.MsgEmote, MsgType: event.MsgEmote,
@@ -507,15 +542,24 @@ func (portal *Portal) convertDiscordTextMessage(intent *appservice.IntentAPI, ms
} }
previews := make([]*BeeperLinkPreview, 0) previews := make([]*BeeperLinkPreview, 0)
for i, embed := range msg.Embeds { for i, embed := range msg.Embeds {
if i == 0 && msg.MessageReference == nil && isReplyEmbed(embed) {
continue
}
with := log.With().
Str("embed_type", string(embed.Type)).
Int("embed_index", i)
switch getEmbedType(embed) { switch getEmbedType(embed) {
case EmbedRich: case EmbedRich:
htmlParts = append(htmlParts, portal.convertDiscordRichEmbed(intent, embed, msg.ID, i)) log := with.Str("computed_embed_type", "rich").Logger()
htmlParts = append(htmlParts, portal.convertDiscordRichEmbed(log.WithContext(ctx), intent, embed, msg.ID, i))
case EmbedLinkPreview: case EmbedLinkPreview:
previews = append(previews, portal.convertDiscordLinkEmbedToBeeper(intent, embed)) log := with.Str("computed_embed_type", "link preview").Logger()
previews = append(previews, portal.convertDiscordLinkEmbedToBeeper(log.WithContext(ctx), intent, embed))
case EmbedVideo: case EmbedVideo:
// Ignore video embeds, they're handled as separate messages // Ignore video embeds, they're handled as separate messages
default: default:
portal.log.Warnfln("Unknown type %s in embed #%d of message %s", embed.Type, i+1, msg.ID) log := with.Logger()
log.Warn().Msg("Unknown embed type in message")
} }
} }

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strings"
"sync" "sync"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
@@ -10,6 +11,7 @@ import (
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database" "go.mau.fi/mautrix-discord/database"
@@ -156,6 +158,18 @@ 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
} }
@@ -204,7 +218,7 @@ func (puppet *Puppet) UpdateName(info *discordgo.User) bool {
puppet.log.Warn().Err(err).Msg("Failed to update displayname") puppet.log.Warn().Err(err).Msg("Failed to update displayname")
} else { } else {
go puppet.updatePortalMeta(func(portal *Portal) { go puppet.updatePortalMeta(func(portal *Portal) {
if portal.UpdateNameDirect(puppet.Name) { if portal.UpdateNameDirect(puppet.Name, false) {
portal.Update() portal.Update()
portal.UpdateBridgeInfo() portal.UpdateBridgeInfo()
} }
@@ -223,12 +237,21 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
puppet.AvatarSet = false puppet.AvatarSet = false
puppet.AvatarURL = id.ContentURI{} puppet.AvatarURL = id.ContentURI{}
// TODO should we just use discord's default avatars for users with no avatar?
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) { if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
url, err := uploadAvatar(puppet.DefaultIntent(), info.AvatarURL("")) downloadURL := discordgo.EndpointUserAvatar(info.ID, info.Avatar)
if err != nil { ext := "png"
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar") if strings.HasPrefix(info.Avatar, "a_") {
return true downloadURL = discordgo.EndpointUserAvatarAnimated(info.ID, info.Avatar)
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,9 +294,53 @@ func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User) {
} }
changed := false changed := false
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
if changed { if changed {
puppet.Update() puppet.Update()
} }
} }
func (puppet *Puppet) UpdateContactInfo(info *discordgo.User) bool {
changed := false
if puppet.Username != info.Username {
puppet.Username = info.Username
changed = true
}
if puppet.Discriminator != info.Discriminator {
puppet.Discriminator = info.Discriminator
changed = true
}
if puppet.IsBot != info.Bot {
puppet.IsBot = info.Bot
changed = true
}
if changed {
puppet.ContactInfoSet = false
puppet.ResendContactInfo()
return true
}
return false
}
func (puppet *Puppet) ResendContactInfo() {
if puppet.bridge.Config.Homeserver.Software != bridgeconfig.SoftwareHungry || puppet.ContactInfoSet {
return
}
contactInfo := map[string]any{
"com.beeper.bridge.identifiers": []string{
fmt.Sprintf("discord:%s#%s", puppet.Username, puppet.Discriminator),
},
"com.beeper.bridge.remote_id": puppet.ID,
"com.beeper.bridge.service": puppet.bridge.BeeperServiceName,
"com.beeper.bridge.network": puppet.bridge.BeeperNetworkName,
"com.beeper.bridge.is_network_bot": puppet.IsBot,
}
err := puppet.DefaultIntent().BeeperUpdateProfile(contactInfo)
if err != nil {
puppet.log.Warn().Err(err).Msg("Failed to store custom contact info in profile")
} else {
puppet.ContactInfoSet = true
}
}

316
user.go
View File

@@ -6,6 +6,7 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -15,6 +16,7 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"maunium.net/go/mautrix/util/dbutil"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
@@ -61,6 +63,8 @@ type User struct {
pendingInteractionsLock sync.Mutex pendingInteractionsLock sync.Mutex
nextDiscordUploadID atomic.Int32 nextDiscordUploadID atomic.Int32
relationships map[string]*discordgo.Relationship
} }
func (user *User) GetRemoteID() string { func (user *User) GetRemoteID() string {
@@ -76,20 +80,24 @@ func (user *User) GetRemoteName() string {
var discordLog zerolog.Logger var discordLog zerolog.Logger
func discordToZeroLevel(level int) zerolog.Level {
switch level {
case discordgo.LogError:
return zerolog.ErrorLevel
case discordgo.LogWarning:
return zerolog.WarnLevel
case discordgo.LogInformational:
return zerolog.InfoLevel
case discordgo.LogDebug:
fallthrough
default:
return zerolog.DebugLevel
}
}
func init() { func init() {
discordgo.Logger = func(msgL, caller int, format string, a ...interface{}) { discordgo.Logger = func(msgL, caller int, format string, a ...interface{}) {
var level zerolog.Level discordLog.WithLevel(discordToZeroLevel(msgL)).Caller(caller+1).Msgf(strings.TrimSpace(format), a...)
switch msgL {
case discordgo.LogError:
level = zerolog.ErrorLevel
case discordgo.LogWarning:
level = zerolog.WarnLevel
case discordgo.LogInformational:
level = zerolog.InfoLevel
case discordgo.LogDebug:
level = zerolog.DebugLevel
}
discordLog.WithLevel(level).Caller(caller+1).Msgf(strings.TrimSpace(format), a...)
} }
} }
@@ -188,6 +196,8 @@ func (br *DiscordBridge) NewUser(dbUser *database.User) *User {
PermissionLevel: br.Config.Bridge.Permissions.Get(dbUser.MXID), PermissionLevel: br.Config.Bridge.Permissions.Get(dbUser.MXID),
pendingInteractions: make(map[string]*WrappedCommandEvent), pendingInteractions: make(map[string]*WrappedCommandEvent),
relationships: make(map[string]*discordgo.Relationship),
} }
user.nextDiscordUploadID.Store(rand.Int31n(100)) user.nextDiscordUploadID.Store(rand.Int31n(100))
user.BridgeState = br.NewBridgeStateQueue(user) user.BridgeState = br.NewBridgeStateQueue(user)
@@ -241,7 +251,7 @@ func (user *User) startupTryConnect(retryCount int) {
user.log.Error().Err(err).Msg("Error connecting on startup") user.log.Error().Err(err).Msg("Error connecting on startup")
closeErr := &websocket.CloseError{} closeErr := &websocket.CloseError{}
if errors.As(err, &closeErr) && closeErr.Code == 4004 { if errors.As(err, &closeErr) && closeErr.Code == 4004 {
user.invalidAuthHandler(nil, nil) user.invalidAuthHandler(nil)
} else if retryCount < 6 { } else if retryCount < 6 {
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-unknown-websocket-error", Message: err.Error()}) user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-unknown-websocket-error", Message: err.Error()})
retryInSeconds := 2 << retryCount retryInSeconds := 2 << retryCount
@@ -323,7 +333,7 @@ func (user *User) getSpaceRoom(ptr *id.RoomID, name, topic string, parent id.Roo
} else { } else {
*ptr = resp.RoomID *ptr = resp.RoomID
user.Update() user.Update()
user.ensureInvited(nil, *ptr, false) user.ensureInvited(nil, *ptr, false, true)
if parent != "" { if parent != "" {
_, err = user.bridge.Bot.SendStateEvent(parent, event.StateSpaceChild, resp.RoomID.String(), &event.SpaceChildEventContent{ _, err = user.bridge.Bot.SendStateEvent(parent, event.StateSpaceChild, resp.RoomID.String(), &event.SpaceChildEventContent{
@@ -429,7 +439,7 @@ func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool)
} }
// TODO sync mute status properly // TODO sync mute status properly
if portal.GuildID != "" && user.bridge.Config.Bridge.MuteChannelsOnCreate { if portal.GuildID != "" && user.bridge.Config.Bridge.MuteChannelsOnCreate && justCreated {
user.mutePortal(doublePuppetIntent, portal, false) user.mutePortal(doublePuppetIntent, portal, false)
} }
} }
@@ -555,44 +565,90 @@ func (user *User) Connect() error {
// TODO move to config // TODO move to config
if os.Getenv("DISCORD_DEBUG") == "1" { if os.Getenv("DISCORD_DEBUG") == "1" {
session.LogLevel = discordgo.LogDebug session.LogLevel = discordgo.LogDebug
} else {
session.LogLevel = discordgo.LogInformational
}
userDiscordLog := user.log.With().Str("component", "discordgo").Logger()
session.Logger = func(msgL, caller int, format string, a ...interface{}) {
userDiscordLog.WithLevel(discordToZeroLevel(msgL)).Caller(caller+1).Msgf(strings.TrimSpace(format), a...)
} }
if !session.IsUser { if !session.IsUser {
session.Identify.Intents = BotIntents session.Identify.Intents = BotIntents
} }
session.EventHandler = user.eventHandlerSync
user.Session = session user.Session = session
user.Session.AddHandler(user.readyHandler)
user.Session.AddHandler(user.resumeHandler)
user.Session.AddHandler(user.connectedHandler)
user.Session.AddHandler(user.disconnectedHandler)
user.Session.AddHandler(user.invalidAuthHandler)
user.Session.AddHandler(user.guildCreateHandler)
user.Session.AddHandler(user.guildDeleteHandler)
user.Session.AddHandler(user.guildUpdateHandler)
user.Session.AddHandler(user.guildRoleCreateHandler)
user.Session.AddHandler(user.guildRoleUpdateHandler)
user.Session.AddHandler(user.guildRoleDeleteHandler)
user.Session.AddHandler(user.channelCreateHandler)
user.Session.AddHandler(user.channelDeleteHandler)
user.Session.AddHandler(user.channelPinsUpdateHandler)
user.Session.AddHandler(user.channelUpdateHandler)
user.Session.AddHandler(user.messageCreateHandler)
user.Session.AddHandler(user.messageDeleteHandler)
user.Session.AddHandler(user.messageUpdateHandler)
user.Session.AddHandler(user.reactionAddHandler)
user.Session.AddHandler(user.reactionRemoveHandler)
user.Session.AddHandler(user.messageAckHandler)
user.Session.AddHandler(user.typingStartHandler)
user.Session.AddHandler(user.interactionSuccessHandler)
return user.Session.Open() return user.Session.Open()
} }
func (user *User) eventHandlerSync(rawEvt any) {
go user.eventHandler(rawEvt)
}
func (user *User) eventHandler(rawEvt any) {
switch evt := rawEvt.(type) {
case *discordgo.Ready:
user.readyHandler(evt)
case *discordgo.Resumed:
user.resumeHandler(evt)
case *discordgo.Connect:
user.connectedHandler(evt)
case *discordgo.Disconnect:
user.disconnectedHandler(evt)
case *discordgo.InvalidAuth:
user.invalidAuthHandler(evt)
case *discordgo.GuildCreate:
user.guildCreateHandler(evt)
case *discordgo.GuildDelete:
user.guildDeleteHandler(evt)
case *discordgo.GuildUpdate:
user.guildUpdateHandler(evt)
case *discordgo.GuildRoleCreate:
user.discordRoleToDB(evt.GuildID, evt.Role, nil, nil)
case *discordgo.GuildRoleUpdate:
user.discordRoleToDB(evt.GuildID, evt.Role, nil, nil)
case *discordgo.GuildRoleDelete:
user.bridge.DB.Role.DeleteByID(evt.GuildID, evt.RoleID)
case *discordgo.ChannelCreate:
user.channelCreateHandler(evt)
case *discordgo.ChannelDelete:
user.channelDeleteHandler(evt)
case *discordgo.ChannelUpdate:
user.channelUpdateHandler(evt)
case *discordgo.ChannelRecipientAdd:
user.channelRecipientAdd(evt)
case *discordgo.ChannelRecipientRemove:
user.channelRecipientRemove(evt)
case *discordgo.RelationshipAdd:
user.relationshipAddHandler(evt)
case *discordgo.RelationshipRemove:
user.relationshipRemoveHandler(evt)
case *discordgo.RelationshipUpdate:
user.relationshipUpdateHandler(evt)
case *discordgo.MessageCreate:
user.pushPortalMessage(evt, "message create", evt.ChannelID, evt.GuildID)
case *discordgo.MessageDelete:
user.pushPortalMessage(evt, "message delete", evt.ChannelID, evt.GuildID)
case *discordgo.MessageUpdate:
user.pushPortalMessage(evt, "message update", evt.ChannelID, evt.GuildID)
case *discordgo.MessageReactionAdd:
user.pushPortalMessage(evt, "reaction add", evt.ChannelID, evt.GuildID)
case *discordgo.MessageReactionRemove:
user.pushPortalMessage(evt, "reaction remove", evt.ChannelID, evt.GuildID)
case *discordgo.MessageAck:
user.messageAckHandler(evt)
case *discordgo.TypingStart:
user.typingStartHandler(evt)
case *discordgo.InteractionSuccess:
user.interactionSuccessHandler(evt)
case *discordgo.Event:
// Ignore
default:
user.log.Debug().Type("event_type", evt).Msg("Unhandled event")
}
}
func (user *User) Disconnect() error { func (user *User) Disconnect() error {
user.Lock() user.Lock()
defer user.Unlock() defer user.Unlock()
@@ -619,7 +675,24 @@ func (user *User) getGuildBridgingMode(guildID string) database.GuildBridgingMod
return guild.BridgingMode return guild.BridgingMode
} }
func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) { type ChannelSlice []*discordgo.Channel
func (s ChannelSlice) Len() int {
return len(s)
}
func (s ChannelSlice) Less(i, j int) bool {
if s[i].Position != 0 || s[j].Position != 0 {
return s[i].Position < s[j].Position
}
return compareMessageIDs(s[i].LastMessageID, s[j].LastMessageID) == 1
}
func (s ChannelSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (user *User) readyHandler(r *discordgo.Ready) {
user.log.Debug().Msg("Discord connection ready") user.log.Debug().Msg("Discord connection ready")
user.bridgeStateLock.Lock() user.bridgeStateLock.Lock()
user.wasLoggedOut = false user.wasLoggedOut = false
@@ -642,6 +715,10 @@ func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBackfilling}) user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBackfilling})
user.tryAutomaticDoublePuppeting() user.tryAutomaticDoublePuppeting()
for _, relationship := range r.Relationships {
user.relationships[relationship.ID] = relationship
}
updateTS := time.Now() updateTS := time.Now()
portalsInSpace := make(map[string]bool) portalsInSpace := make(map[string]bool)
for _, guild := range user.GetPortals() { for _, guild := range user.GetPortals() {
@@ -650,6 +727,8 @@ func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
for _, guild := range r.Guilds { for _, guild := range r.Guilds {
user.handleGuild(guild, updateTS, portalsInSpace[guild.ID]) user.handleGuild(guild, updateTS, portalsInSpace[guild.ID])
} }
// The private channel list doesn't seem to be sorted by default, so sort it by message IDs (highest=newest first)
sort.Sort(ChannelSlice(r.PrivateChannels))
for i, ch := range r.PrivateChannels { for i, ch := range r.PrivateChannels {
portal := user.GetPortalByMeta(ch) portal := user.GetPortalByMeta(ch)
user.handlePrivateChannel(portal, ch, updateTS, i < user.bridge.Config.Bridge.PrivateChannelCreateLimit, portalsInSpace[portal.Key.ChannelID]) user.handlePrivateChannel(portal, ch, updateTS, i < user.bridge.Config.Bridge.PrivateChannelCreateLimit, portalsInSpace[portal.Key.ChannelID])
@@ -659,7 +738,7 @@ func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
if r.ReadState != nil && r.ReadState.Version > user.ReadStateVersion { if r.ReadState != nil && r.ReadState.Version > user.ReadStateVersion {
// TODO can we figure out which read states are actually new? // TODO can we figure out which read states are actually new?
for _, entry := range r.ReadState.Entries { for _, entry := range r.ReadState.Entries {
user.messageAckHandler(nil, &discordgo.MessageAck{ user.messageAckHandler(&discordgo.MessageAck{
MessageID: string(entry.LastMessageID), MessageID: string(entry.LastMessageID),
ChannelID: entry.ID, ChannelID: entry.ID,
}) })
@@ -696,7 +775,7 @@ func (user *User) subscribeGuilds(delay time.Duration) {
} }
} }
func (user *User) resumeHandler(_ *discordgo.Session, r *discordgo.Resumed) { func (user *User) resumeHandler(_ *discordgo.Resumed) {
user.log.Debug().Msg("Discord connection resumed") user.log.Debug().Msg("Discord connection resumed")
user.subscribeGuilds(0 * time.Second) user.subscribeGuilds(0 * time.Second)
} }
@@ -718,6 +797,55 @@ func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
} }
} }
func (user *User) relationshipAddHandler(r *discordgo.RelationshipAdd) {
user.log.Debug().Interface("relationship", r.Relationship).Msg("Relationship added")
user.relationships[r.ID] = r.Relationship
user.handleRelationshipChange(r.ID, r.Nickname)
}
func (user *User) relationshipUpdateHandler(r *discordgo.RelationshipUpdate) {
user.log.Debug().Interface("relationship", r.Relationship).Msg("Relationship update")
user.relationships[r.ID] = r.Relationship
user.handleRelationshipChange(r.ID, r.Nickname)
}
func (user *User) relationshipRemoveHandler(r *discordgo.RelationshipRemove) {
user.log.Debug().Str("other_user_id", r.ID).Msg("Relationship removed")
delete(user.relationships, r.ID)
user.handleRelationshipChange(r.ID, "")
}
func (user *User) handleRelationshipChange(userID, nickname string) {
puppet := user.bridge.GetPuppetByID(userID)
portal := user.FindPrivateChatWith(userID)
if portal == nil || puppet == nil {
return
}
updated := portal.FriendNick == (nickname != "")
portal.FriendNick = nickname != ""
if nickname != "" {
updated = portal.UpdateNameDirect(nickname, true)
} else if portal.Name != puppet.Name {
if portal.shouldSetDMRoomMetadata() {
updated = portal.UpdateNameDirect(puppet.Name, false)
} else if portal.NameSet {
_, err := portal.MainIntent().SendStateEvent(portal.MXID, event.StateRoomName, "", map[string]any{})
if err != nil {
portal.log.Warn().Err(err).Msg("Failed to clear room name after friend nickname was removed")
} else {
portal.log.Debug().Msg("Cleared room name after friend nickname was removed")
portal.NameSet = false
portal.Update()
updated = true
}
}
}
if !updated {
portal.Update()
}
}
func (user *User) handlePrivateChannel(portal *Portal, meta *discordgo.Channel, timestamp time.Time, create, isInSpace bool) { func (user *User) handlePrivateChannel(portal *Portal, meta *discordgo.Channel, timestamp time.Time, create, isInSpace bool) {
if create && portal.MXID == "" { if create && portal.MXID == "" {
err := portal.CreateMatrixRoom(user, meta) err := portal.CreateMatrixRoom(user, meta)
@@ -760,7 +888,7 @@ func (user *User) addGuildToSpace(guild *Guild, isInSpace bool, timestamp time.T
return isInSpace return isInSpace
} }
func (user *User) discordRoleToDB(guildID string, role *discordgo.Role, dbRole *database.Role) (*database.Role, bool) { func (user *User) discordRoleToDB(guildID string, role *discordgo.Role, dbRole *database.Role, txn dbutil.Execable) bool {
var changed bool var changed bool
if dbRole == nil { if dbRole == nil {
dbRole = user.bridge.DB.Role.New() dbRole = user.bridge.DB.Role.New()
@@ -778,7 +906,10 @@ func (user *User) discordRoleToDB(guildID string, role *discordgo.Role, dbRole *
dbRole.Permissions != role.Permissions dbRole.Permissions != role.Permissions
} }
dbRole.Role = *role dbRole.Role = *role
return dbRole, changed if changed {
dbRole.Upsert(txn)
}
return changed
} }
func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) { func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) {
@@ -793,11 +924,8 @@ func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) {
panic(err) panic(err)
} }
for _, role := range newRoles { for _, role := range newRoles {
dbRole, changed := user.discordRoleToDB(guildID, role, existingRoleMap[role.ID]) user.discordRoleToDB(guildID, role, existingRoleMap[role.ID], txn)
delete(existingRoleMap, role.ID) delete(existingRoleMap, role.ID)
if changed {
dbRole.Upsert(txn)
}
} }
for _, removeRole := range existingRoleMap { for _, removeRole := range existingRoleMap {
removeRole.Delete(txn) removeRole.Delete(txn)
@@ -813,20 +941,6 @@ func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) {
} }
} }
func (user *User) guildRoleCreateHandler(_ *discordgo.Session, r *discordgo.GuildRoleCreate) {
dbRole, _ := user.discordRoleToDB(r.GuildID, r.Role, nil)
dbRole.Upsert(nil)
}
func (user *User) guildRoleUpdateHandler(_ *discordgo.Session, r *discordgo.GuildRoleUpdate) {
dbRole, _ := user.discordRoleToDB(r.GuildID, r.Role, nil)
dbRole.Upsert(nil)
}
func (user *User) guildRoleDeleteHandler(_ *discordgo.Session, r *discordgo.GuildRoleDelete) {
user.bridge.DB.Role.DeleteByID(r.GuildID, r.RoleID)
}
func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSpace bool) { func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSpace bool) {
guild := user.bridge.GetGuildByID(meta.ID, true) guild := user.bridge.GetGuildByID(meta.ID, true)
guild.UpdateInfo(user, meta) guild.UpdateInfo(user, meta)
@@ -855,7 +969,7 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp
user.addGuildToSpace(guild, isInSpace, timestamp) user.addGuildToSpace(guild, isInSpace, timestamp)
} }
func (user *User) connectedHandler(_ *discordgo.Session, _ *discordgo.Connect) { func (user *User) connectedHandler(_ *discordgo.Connect) {
user.bridgeStateLock.Lock() user.bridgeStateLock.Lock()
defer user.bridgeStateLock.Unlock() defer user.bridgeStateLock.Unlock()
user.log.Debug().Msg("Connected to Discord") user.log.Debug().Msg("Connected to Discord")
@@ -865,7 +979,7 @@ func (user *User) connectedHandler(_ *discordgo.Session, _ *discordgo.Connect) {
} }
} }
func (user *User) disconnectedHandler(_ *discordgo.Session, _ *discordgo.Disconnect) { func (user *User) disconnectedHandler(_ *discordgo.Disconnect) {
user.bridgeStateLock.Lock() user.bridgeStateLock.Lock()
defer user.bridgeStateLock.Unlock() defer user.bridgeStateLock.Unlock()
if user.wasLoggedOut { if user.wasLoggedOut {
@@ -877,7 +991,7 @@ func (user *User) disconnectedHandler(_ *discordgo.Session, _ *discordgo.Disconn
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-transient-disconnect", Message: "Temporarily disconnected from Discord, trying to reconnect"}) user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-transient-disconnect", Message: "Temporarily disconnected from Discord, trying to reconnect"})
} }
func (user *User) invalidAuthHandler(_ *discordgo.Session, _ *discordgo.InvalidAuth) { func (user *User) invalidAuthHandler(_ *discordgo.InvalidAuth) {
user.bridgeStateLock.Lock() user.bridgeStateLock.Lock()
defer user.bridgeStateLock.Unlock() defer user.bridgeStateLock.Unlock()
user.log.Info().Msg("Got logged out from Discord due to invalid token") user.log.Info().Msg("Got logged out from Discord due to invalid token")
@@ -886,7 +1000,7 @@ func (user *User) invalidAuthHandler(_ *discordgo.Session, _ *discordgo.InvalidA
go user.Logout(false) go user.Logout(false)
} }
func (user *User) guildCreateHandler(_ *discordgo.Session, g *discordgo.GuildCreate) { func (user *User) guildCreateHandler(g *discordgo.GuildCreate) {
user.log.Info(). user.log.Info().
Str("guild_id", g.ID). Str("guild_id", g.ID).
Str("name", g.Name). Str("name", g.Name).
@@ -895,7 +1009,7 @@ func (user *User) guildCreateHandler(_ *discordgo.Session, g *discordgo.GuildCre
user.handleGuild(g.Guild, time.Now(), false) user.handleGuild(g.Guild, time.Now(), false)
} }
func (user *User) guildDeleteHandler(_ *discordgo.Session, g *discordgo.GuildDelete) { func (user *User) guildDeleteHandler(g *discordgo.GuildDelete) {
user.log.Info().Str("guild_id", g.ID).Msg("Got guild delete event") user.log.Info().Str("guild_id", g.ID).Msg("Got guild delete event")
user.MarkNotInPortal(g.ID) user.MarkNotInPortal(g.ID)
guild := user.bridge.GetGuildByID(g.ID, false) guild := user.bridge.GetGuildByID(g.ID, false)
@@ -911,12 +1025,12 @@ func (user *User) guildDeleteHandler(_ *discordgo.Session, g *discordgo.GuildDel
} }
} }
func (user *User) guildUpdateHandler(_ *discordgo.Session, g *discordgo.GuildUpdate) { func (user *User) guildUpdateHandler(g *discordgo.GuildUpdate) {
user.log.Debug().Str("guild_id", g.ID).Msg("Got guild update event") user.log.Debug().Str("guild_id", g.ID).Msg("Got guild update event")
user.handleGuild(g.Guild, time.Now(), user.IsInSpace(g.ID)) user.handleGuild(g.Guild, time.Now(), user.IsInSpace(g.ID))
} }
func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.ChannelCreate) { func (user *User) channelCreateHandler(c *discordgo.ChannelCreate) {
if user.getGuildBridgingMode(c.GuildID) < database.GuildBridgeEverything { if user.getGuildBridgingMode(c.GuildID) < database.GuildBridgeEverything {
user.log.Debug(). user.log.Debug().
Str("guild_id", c.GuildID).Str("channel_id", c.ID). Str("guild_id", c.GuildID).Str("channel_id", c.ID).
@@ -946,7 +1060,7 @@ func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.Channe
} }
} }
func (user *User) channelDeleteHandler(_ *discordgo.Session, c *discordgo.ChannelDelete) { func (user *User) channelDeleteHandler(c *discordgo.ChannelDelete) {
portal := user.GetExistingPortalByID(c.ID) portal := user.GetExistingPortalByID(c.ID)
if portal == nil { if portal == nil {
user.log.Debug(). user.log.Debug().
@@ -967,11 +1081,7 @@ func (user *User) channelDeleteHandler(_ *discordgo.Session, c *discordgo.Channe
Msg("Completed cleaning up channel") Msg("Completed cleaning up channel")
} }
func (user *User) channelPinsUpdateHandler(_ *discordgo.Session, c *discordgo.ChannelPinsUpdate) { func (user *User) channelUpdateHandler(c *discordgo.ChannelUpdate) {
user.log.Debug().Msg("channel pins update")
}
func (user *User) channelUpdateHandler(_ *discordgo.Session, c *discordgo.ChannelUpdate) {
portal := user.GetPortalByMeta(c.Channel) portal := user.GetPortalByMeta(c.Channel)
if c.GuildID == "" { if c.GuildID == "" {
user.handlePrivateChannel(portal, c.Channel, time.Now(), true, user.IsInSpace(portal.Key.String())) user.handlePrivateChannel(portal, c.Channel, time.Now(), true, user.IsInSpace(portal.Key.String()))
@@ -980,6 +1090,20 @@ func (user *User) channelUpdateHandler(_ *discordgo.Session, c *discordgo.Channe
} }
} }
func (user *User) channelRecipientAdd(c *discordgo.ChannelRecipientAdd) {
portal := user.GetExistingPortalByID(c.ChannelID)
if portal != nil {
portal.syncParticipant(user, c.User, false)
}
}
func (user *User) channelRecipientRemove(c *discordgo.ChannelRecipientRemove) {
portal := user.GetExistingPortalByID(c.ChannelID)
if portal != nil {
portal.syncParticipant(user, c.User, true)
}
}
func (user *User) findPortal(channelID string) (*Portal, *Thread) { func (user *User) findPortal(channelID string) (*Portal, *Thread) {
portal := user.GetExistingPortalByID(channelID) portal := user.GetExistingPortalByID(channelID)
if portal != nil { if portal != nil {
@@ -1044,26 +1168,6 @@ func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildI
} }
} }
func (user *User) messageCreateHandler(_ *discordgo.Session, m *discordgo.MessageCreate) {
user.pushPortalMessage(m, "message create", m.ChannelID, m.GuildID)
}
func (user *User) messageDeleteHandler(_ *discordgo.Session, m *discordgo.MessageDelete) {
user.pushPortalMessage(m, "message delete", m.ChannelID, m.GuildID)
}
func (user *User) messageUpdateHandler(_ *discordgo.Session, m *discordgo.MessageUpdate) {
user.pushPortalMessage(m, "message update", m.ChannelID, m.GuildID)
}
func (user *User) reactionAddHandler(_ *discordgo.Session, m *discordgo.MessageReactionAdd) {
user.pushPortalMessage(m, "reaction add", m.ChannelID, m.GuildID)
}
func (user *User) reactionRemoveHandler(_ *discordgo.Session, m *discordgo.MessageReactionRemove) {
user.pushPortalMessage(m, "reaction remove", m.ChannelID, m.GuildID)
}
type CustomReadReceipt struct { type CustomReadReceipt struct {
Timestamp int64 `json:"ts,omitempty"` Timestamp int64 `json:"ts,omitempty"`
DoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"` DoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"`
@@ -1088,7 +1192,7 @@ func (user *User) makeReadMarkerContent(eventID id.EventID) *CustomReadMarkers {
} }
} }
func (user *User) messageAckHandler(_ *discordgo.Session, m *discordgo.MessageAck) { func (user *User) messageAckHandler(m *discordgo.MessageAck) {
portal := user.GetExistingPortalByID(m.ChannelID) portal := user.GetExistingPortalByID(m.ChannelID)
if portal == nil || portal.MXID == "" { if portal == nil || portal.MXID == "" {
return return
@@ -1121,7 +1225,7 @@ func (user *User) messageAckHandler(_ *discordgo.Session, m *discordgo.MessageAc
} }
} }
func (user *User) typingStartHandler(_ *discordgo.Session, t *discordgo.TypingStart) { func (user *User) typingStartHandler(t *discordgo.TypingStart) {
portal := user.GetExistingPortalByID(t.ChannelID) portal := user.GetExistingPortalByID(t.ChannelID)
if portal == nil || portal.MXID == "" { if portal == nil || portal.MXID == "" {
return return
@@ -1129,7 +1233,7 @@ func (user *User) typingStartHandler(_ *discordgo.Session, t *discordgo.TypingSt
portal.handleDiscordTyping(t) portal.handleDiscordTyping(t)
} }
func (user *User) interactionSuccessHandler(_ *discordgo.Session, s *discordgo.InteractionSuccess) { func (user *User) interactionSuccessHandler(s *discordgo.InteractionSuccess) {
user.pendingInteractionsLock.Lock() user.pendingInteractionsLock.Lock()
defer user.pendingInteractionsLock.Unlock() defer user.pendingInteractionsLock.Unlock()
ce, ok := user.pendingInteractions[s.Nonce] ce, ok := user.pendingInteractions[s.Nonce]
@@ -1142,10 +1246,16 @@ func (user *User) interactionSuccessHandler(_ *discordgo.Session, s *discordgo.I
} }
} }
func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) bool { func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect, ignoreCache bool) bool {
if roomID == "" {
return false
}
if intent == nil { if intent == nil {
intent = user.bridge.Bot intent = user.bridge.Bot
} }
if !ignoreCache && intent.StateStore.IsInvited(roomID, user.MXID) {
return true
}
ret := false ret := false
inviteContent := event.Content{ inviteContent := event.Content{