36 Commits

Author SHA1 Message Date
Tulir Asokan
fb6d89a88f Bump version to v0.6.1 2023-08-17 00:57:00 +03:00
Tulir Asokan
acaaa9f0f8 Update dependencies 2023-08-17 00:54:38 +03:00
Tulir Asokan
2ec3b0ebce Update discordgo 2023-08-04 18:42:02 +03:00
Tulir Asokan
802ec555d6 Update discordgo to remove need to fetch own member info manually 2023-08-04 14:16:36 +03:00
Tulir Asokan
84a6fbc571 Move channelIsBridgeable check when syncing guild channels
Fixes #107
2023-08-04 13:47:25 +03:00
Tulir Asokan
0391750fea Fix handling gifs where canonical URL is different 2023-08-03 00:17:15 +03:00
Tulir Asokan
5467ab074d Update mautrix-go 2023-07-29 14:43:44 +03:00
Tulir Asokan
ff0a9bcafa Update mautrix-go 2023-07-22 20:35:36 +03:00
Tulir Asokan
aef54fcc3b Update usernames in login/ping commands 2023-07-18 22:58:59 +03:00
Tulir Asokan
dab1aba6e5 Bump version to v0.6.0 2023-07-16 12:57:13 +03:00
Tulir Asokan
792ad54b9c Fix error messages in portals with no relay webhook 2023-07-15 18:55:16 +03:00
Tulir Asokan
9b7b60966f Redact relay webhook secret in error messages. Fixes #105 2023-07-15 18:53:01 +03:00
Tulir Asokan
104ee2da57 Fix panic if lottieconverter isn't installed 2023-07-03 17:09:26 +03:00
Tulir Asokan
41d0ffcf3b Update changelog 2023-07-03 17:07:54 +03:00
Tulir Asokan
b87421f0fb Ignore guild delete events with unavailable=true 2023-06-30 22:20:32 +03:00
Tulir Asokan
3c4561113b Remove long wait for semaphore 2023-06-30 15:04:47 +03:00
Tulir Asokan
3eb5c44be3 Fix attachment semaphore unlocking when download fails 2023-06-30 15:03:50 +03:00
Tulir Asokan
a67d6d2af7 Add italics for bridging emotes 2023-06-29 15:23:44 +03:00
Tulir Asokan
f4284e7b3f Prevent attachment semaphore from blocking permanently 2023-06-29 15:19:52 +03:00
Tulir Asokan
07785997bf Add some debug logs for backfill lock 2023-06-29 15:19:52 +03:00
Sumner Evans
62a1d83508 deps/mautrix: upgrade to reduce logs on database transactions
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-06-28 15:33:06 -06:00
Sumner Evans
57b7be8cbb logging: remove 'Starting' log and use duration instead
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-06-27 09:22:13 -06:00
Sumner Evans
f5ffbe1311 deps/mautrix: upgrade to reduce logs of requests
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-06-27 09:22:13 -06:00
Tulir Asokan
be1128fd50 Update Docker image to Alpine 3.18 2023-06-26 13:21:44 +03:00
Tulir Asokan
b4249488db Prevent handling too many attachments in parallel 2023-06-23 15:32:18 +03:00
Tulir Asokan
b446d865d0 Update mautrix-go 2023-06-23 15:20:11 +03:00
Tulir Asokan
25d07c9c34 Log event IDs after handling message 2023-06-22 13:18:32 +03:00
Tulir Asokan
200c4fc9d0 Expose Application flag to displayname templates
Fixes #94
2023-06-22 13:18:27 +03:00
Tulir Asokan
d39499cdcf Update username format in custom bridge identifier metadata 2023-06-20 16:32:25 +03:00
Tulir Asokan
c449696120 Handle usernames properly in bridge state remote name 2023-06-20 15:29:46 +03:00
Tulir Asokan
914b360720 Switch to new beeper batch send endpoint 2023-06-19 14:55:44 +03:00
Tulir Asokan
11b91dc299 Backfill threads when found and from server thread list sync 2023-06-18 22:13:20 +03:00
Tulir Asokan
b77eea4586 Create threads for backfilled messages 2023-06-18 20:49:27 +03:00
Tulir Asokan
8ebad277f5 Make backfilling code compatible with threads
This doesn't trigger thread backfill yet, but the backfill methods can
handle threads now.
2023-06-18 20:09:23 +03:00
Tulir Asokan
248664f8b0 Set guild bridging mode when using bridge command without entire flag 2023-06-17 19:37:21 +03:00
Tulir Asokan
3247709abb Improve logs and fix things with avatar reuploads 2023-06-17 19:37:08 +03:00
34 changed files with 500 additions and 210 deletions

View File

@@ -1,3 +1,25 @@
# v0.6.1 (2023-08-16)
* Bumped minimum Go version to 1.20.
* Fixed all logged-in users being invited to existing portal rooms even if they
don't have permission to view the channel on Discord.
* Fixed gif links not being treated as embeds if the canonical URL is different
than the URL in the message body.
# v0.6.0 (2023-07-16)
* Added initial support for backfilling threads.
* Exposed `Application` flag to displayname template.
* Changed `m.emote` bridging to use italics on Discord.
* Updated Docker image to Alpine 3.18.
* Added limit to parallel media transfers to avoid high memory usage if lots
of messages are received at the same time.
* Fixed guilds being unbridged if Discord has server issues and temporarily
marks a guild as unavailable.
* Fixed using `guilds bridge` command without `--entire` flag.
* Fixed panic if lottieconverter isn't installed.
* Fixed relay webhook secret being leaked in network error messages.
# v0.5.0 (2023-06-16) # v0.5.0 (2023-06-16)
* Added support for intentional mentions in Matrix (MSC3952). * Added support for intentional mentions in Matrix (MSC3952).

View File

@@ -1,6 +1,6 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18 AS lottie
FROM golang:1-alpine3.17 AS builder FROM golang:1-alpine3.18 AS builder
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
@@ -8,7 +8,7 @@ COPY . /build
WORKDIR /build WORKDIR /build
RUN go build -o /usr/bin/mautrix-discord RUN go build -o /usr/bin/mautrix-discord
FROM alpine:3.17 FROM alpine:3.18
ENV UID=1337 \ ENV UID=1337 \
GID=1337 GID=1337

View File

@@ -1,6 +1,6 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18 AS lottie
FROM alpine:3.17 FROM alpine:3.18
ENV UID=1337 \ ENV UID=1337 \
GID=1337 GID=1337

View File

@@ -1,6 +1,6 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18 AS lottie
FROM golang:1-alpine3.17 AS builder FROM golang:1-alpine3.18 AS builder
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl \ RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl \
zlib libpng giflib libstdc++ libgcc zlib libpng giflib libstdc++ libgcc

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"image" "image"
"io" "io"
@@ -12,23 +13,23 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"go.mau.fi/util/exsync"
"go.mau.fi/util/ffmpeg"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util"
"maunium.net/go/mautrix/util/ffmpeg"
"go.mau.fi/mautrix-discord/database" "go.mau.fi/mautrix-discord/database"
) )
func downloadDiscordAttachment(url string) ([]byte, error) { func downloadDiscordAttachment(url string, maxSize int64) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -44,9 +45,24 @@ func downloadDiscordAttachment(url string) ([]byte, error) {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode > 300 { if resp.StatusCode > 300 {
data, _ := io.ReadAll(resp.Body) data, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, data) return nil, fmt.Errorf("unexpected status %d downloading %s: %s", resp.StatusCode, url, data)
}
if resp.Header.Get("Content-Length") != "" {
length, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse content length: %w", err)
} else if length > maxSize {
return nil, fmt.Errorf("attachment too large (%d > %d)", length, maxSize)
}
return io.ReadAll(resp.Body)
} else {
var mbe *http.MaxBytesError
data, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxSize))
if err != nil && errors.As(err, &mbe) {
return nil, fmt.Errorf("attachment too large (over %d)", maxSize)
}
return data, err
} }
return io.ReadAll(resp.Body)
} }
func uploadDiscordAttachment(url string, data []byte) error { func uploadDiscordAttachment(url string, data []byte) error {
@@ -99,7 +115,7 @@ func downloadMatrixAttachment(intent *appservice.IntentAPI, content *event.Messa
return data, nil return data, nil
} }
func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, meta AttachmentMeta) (*database.File, error) { func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, meta AttachmentMeta, semaWg *sync.WaitGroup) (*database.File, error) {
dbFile := br.DB.File.New() dbFile := br.DB.File.New()
dbFile.Timestamp = time.Now() dbFile.Timestamp = time.Now()
dbFile.URL = url dbFile.URL = url
@@ -135,7 +151,9 @@ func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, da
dbFile.MXC = resp.ContentURI dbFile.MXC = resp.ContentURI
req.MXC = resp.ContentURI req.MXC = resp.ContentURI
req.UnstableUploadURL = resp.UnstableUploadURL req.UnstableUploadURL = resp.UnstableUploadURL
semaWg.Add(1)
go func() { go func() {
defer semaWg.Done()
_, err = intent.UploadMedia(req) _, err = intent.UploadMedia(req)
if err != nil { if err != nil {
br.Log.Errorfln("Failed to upload %s: %v", req.MXC, err) br.Log.Errorfln("Failed to upload %s: %v", req.MXC, err)
@@ -250,7 +268,7 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
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}
once, _ := br.attachmentTransfers.GetOrSet(transferKey, &util.ReturnableOnce[*database.File]{}) once, _ := br.attachmentTransfers.GetOrSet(transferKey, &exsync.ReturnableOnce[*database.File]{})
returnDBFile, returnErr = once.Do(func() (onceDBFile *database.File, onceErr error) { returnDBFile, returnErr = once.Do(func() (onceDBFile *database.File, onceErr error) {
if isCacheable { if isCacheable {
onceDBFile = br.DB.File.Get(url, encrypt) onceDBFile = br.DB.File.Get(url, encrypt)
@@ -259,8 +277,25 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
} }
} }
const attachmentSizeVal = 1
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
onceErr = br.parallelAttachmentSemaphore.Acquire(ctx, attachmentSizeVal)
cancel()
if onceErr != nil {
br.ZLog.Warn().Err(onceErr).Msg("Failed to acquire semaphore")
onceErr = fmt.Errorf("reuploading timed out")
return
}
var semaWg sync.WaitGroup
semaWg.Add(1)
defer semaWg.Done()
go func() {
semaWg.Wait()
br.parallelAttachmentSemaphore.Release(attachmentSizeVal)
}()
var data []byte var data []byte
data, onceErr = downloadDiscordAttachment(url) data, onceErr = downloadDiscordAttachment(url, br.MediaConfig.UploadSize)
if onceErr != nil { if onceErr != nil {
return return
} }
@@ -273,7 +308,7 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
} }
} }
onceDBFile, onceErr = br.uploadMatrixAttachment(intent, data, url, encrypt, meta) onceDBFile, onceErr = br.uploadMatrixAttachment(intent, data, url, encrypt, meta, &semaWg)
if onceErr != nil { if onceErr != nil {
return return
} }

View File

@@ -10,15 +10,18 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database" "go.mau.fi/mautrix-discord/database"
) )
func (portal *Portal) forwardBackfillInitial(source *User) { func (portal *Portal) forwardBackfillInitial(source *User, thread *Thread) {
defer portal.forwardBackfillLock.Unlock() log := portal.log
defer func() {
log.Debug().Msg("Forward backfill finished, unlocking lock")
portal.forwardBackfillLock.Unlock()
}()
// This should only be called from CreateMatrixRoom which locks forwardBackfillLock before creating the room. // This should only be called from CreateMatrixRoom which locks forwardBackfillLock before creating the room.
if portal.forwardBackfillLock.TryLock() { if portal.forwardBackfillLock.TryLock() {
panic("forwardBackfillInitial() called without locking forwardBackfillLock") panic("forwardBackfillInitial() called without locking forwardBackfillLock")
@@ -27,21 +30,28 @@ func (portal *Portal) forwardBackfillInitial(source *User) {
limit := portal.bridge.Config.Bridge.Backfill.Limits.Initial.Channel limit := portal.bridge.Config.Bridge.Backfill.Limits.Initial.Channel
if portal.GuildID == "" { if portal.GuildID == "" {
limit = portal.bridge.Config.Bridge.Backfill.Limits.Initial.DM limit = portal.bridge.Config.Bridge.Backfill.Limits.Initial.DM
if thread != nil {
limit = portal.bridge.Config.Bridge.Backfill.Limits.Initial.Thread
thread.initialBackfillAttempted = true
}
} }
if limit == 0 { if limit == 0 {
return return
} }
log := portal.log.With(). with := 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)
Logger() if thread != nil {
with = with.Str("thread_id", thread.ID)
}
log = with.Logger()
portal.backfillLimited(log, source, limit, "") portal.backfillLimited(log, source, limit, "", thread)
} }
func (portal *Portal) ForwardBackfillMissed(source *User, meta *discordgo.Channel) { func (portal *Portal) ForwardBackfillMissed(source *User, serverLastMessageID string, thread *Thread) {
if portal.MXID == "" { if portal.MXID == "" {
return return
} }
@@ -49,50 +59,65 @@ func (portal *Portal) ForwardBackfillMissed(source *User, meta *discordgo.Channe
limit := portal.bridge.Config.Bridge.Backfill.Limits.Missed.Channel limit := portal.bridge.Config.Bridge.Backfill.Limits.Missed.Channel
if portal.GuildID == "" { if portal.GuildID == "" {
limit = portal.bridge.Config.Bridge.Backfill.Limits.Missed.DM limit = portal.bridge.Config.Bridge.Backfill.Limits.Missed.DM
if thread != nil {
limit = portal.bridge.Config.Bridge.Backfill.Limits.Missed.Thread
}
} }
if limit == 0 { if limit == 0 {
return return
} }
log := portal.log.With(). with := 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)
Logger() if thread != nil {
with = with.Str("thread_id", thread.ID)
}
log := with.Logger()
portal.forwardBackfillLock.Lock() portal.forwardBackfillLock.Lock()
defer portal.forwardBackfillLock.Unlock() defer portal.forwardBackfillLock.Unlock()
lastMessage := portal.bridge.DB.Message.GetLast(portal.Key) var lastMessage *database.Message
if lastMessage == nil || meta.LastMessageID == "" { if thread != nil {
lastMessage = portal.bridge.DB.Message.GetLastInThread(portal.Key, thread.ID)
} else {
lastMessage = portal.bridge.DB.Message.GetLast(portal.Key)
}
if lastMessage == nil || serverLastMessageID == "" {
log.Debug().Msg("Not backfilling, no last message in database or no last message in metadata") log.Debug().Msg("Not backfilling, no last message in database or no last message in metadata")
return return
} else if !shouldBackfill(lastMessage.DiscordID, meta.LastMessageID) { } else if !shouldBackfill(lastMessage.DiscordID, serverLastMessageID) {
log.Debug(). log.Debug().
Str("last_bridged_message", lastMessage.DiscordID). Str("last_bridged_message", lastMessage.DiscordID).
Str("last_server_message", meta.LastMessageID). Str("last_server_message", serverLastMessageID).
Msg("Not backfilling, last message in database is newer than last message in metadata") Msg("Not backfilling, last message in database is newer than last message in metadata")
return return
} }
log.Debug(). log.Debug().
Str("last_bridged_message", lastMessage.DiscordID). Str("last_bridged_message", lastMessage.DiscordID).
Str("last_server_message", meta.LastMessageID). Str("last_server_message", serverLastMessageID).
Msg("Backfilling missed messages") Msg("Backfilling missed messages")
if limit < 0 { if limit < 0 {
portal.backfillUnlimitedMissed(log, source, lastMessage.DiscordID) portal.backfillUnlimitedMissed(log, source, lastMessage.DiscordID, thread)
} else { } else {
portal.backfillLimited(log, source, limit, lastMessage.DiscordID) portal.backfillLimited(log, source, limit, lastMessage.DiscordID, thread)
} }
} }
const messageFetchChunkSize = 50 const messageFetchChunkSize = 50
func (portal *Portal) collectBackfillMessages(log zerolog.Logger, source *User, limit int, until string) ([]*discordgo.Message, bool, error) { func (portal *Portal) collectBackfillMessages(log zerolog.Logger, source *User, limit int, until string, thread *Thread) ([]*discordgo.Message, bool, error) {
var messages []*discordgo.Message var messages []*discordgo.Message
var before string var before string
var foundAll bool var foundAll bool
protoChannelID := portal.Key.ChannelID
if thread != nil {
protoChannelID = thread.ID
}
for { for {
log.Debug().Str("before_id", before).Msg("Fetching messages for backfill") log.Debug().Str("before_id", before).Msg("Fetching messages for backfill")
newMessages, err := source.Session.ChannelMessages(portal.Key.ChannelID, messageFetchChunkSize, before, "", "") newMessages, err := source.Session.ChannelMessages(protoChannelID, messageFetchChunkSize, before, "", "")
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@@ -123,8 +148,8 @@ func (portal *Portal) collectBackfillMessages(log zerolog.Logger, source *User,
return messages, foundAll, nil return messages, foundAll, nil
} }
func (portal *Portal) backfillLimited(log zerolog.Logger, source *User, limit int, after string) { func (portal *Portal) backfillLimited(log zerolog.Logger, source *User, limit int, after string, thread *Thread) {
messages, foundAll, err := portal.collectBackfillMessages(log, source, limit, after) messages, foundAll, err := portal.collectBackfillMessages(log, source, limit, after, thread)
if err != nil { if err != nil {
log.Err(err).Msg("Error collecting messages to forward backfill") log.Err(err).Msg("Error collecting messages to forward backfill")
return return
@@ -145,13 +170,17 @@ func (portal *Portal) backfillLimited(log zerolog.Logger, source *User, limit in
log.Debug().Msg("Sent warning about possibly missed messages") log.Debug().Msg("Sent warning about possibly missed messages")
} }
} }
portal.sendBackfillBatch(log, source, messages) portal.sendBackfillBatch(log, source, messages, thread)
} }
func (portal *Portal) backfillUnlimitedMissed(log zerolog.Logger, source *User, after string) { func (portal *Portal) backfillUnlimitedMissed(log zerolog.Logger, source *User, after string, thread *Thread) {
protoChannelID := portal.Key.ChannelID
if thread != nil {
protoChannelID = thread.ID
}
for { for {
log.Debug().Str("after_id", after).Msg("Fetching chunk of messages to backfill") log.Debug().Str("after_id", after).Msg("Fetching chunk of messages to backfill")
messages, err := source.Session.ChannelMessages(portal.Key.ChannelID, messageFetchChunkSize, "", after, "") messages, err := source.Session.ChannelMessages(protoChannelID, messageFetchChunkSize, "", after, "")
if err != nil { if err != nil {
log.Err(err).Msg("Error fetching chunk of messages to forward backfill") log.Err(err).Msg("Error fetching chunk of messages to forward backfill")
return return
@@ -159,7 +188,7 @@ func (portal *Portal) backfillUnlimitedMissed(log zerolog.Logger, source *User,
log.Debug().Int("count", len(messages)).Msg("Fetched chunk of messages to backfill") log.Debug().Int("count", len(messages)).Msg("Fetched chunk of messages to backfill")
sort.Sort(MessageSlice(messages)) sort.Sort(MessageSlice(messages))
portal.sendBackfillBatch(log, source, messages) portal.sendBackfillBatch(log, source, messages, thread)
if len(messages) < messageFetchChunkSize { if len(messages) < messageFetchChunkSize {
// Assume that was all the missing messages // Assume that was all the missing messages
@@ -170,28 +199,28 @@ func (portal *Portal) backfillUnlimitedMissed(log zerolog.Logger, source *User,
} }
} }
func (portal *Portal) sendBackfillBatch(log zerolog.Logger, source *User, messages []*discordgo.Message) { func (portal *Portal) sendBackfillBatch(log zerolog.Logger, source *User, messages []*discordgo.Message, thread *Thread) {
if portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry { if portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureBatchSending) {
log.Debug().Msg("Using hungryserv, sending messages with batch send endpoint") log.Debug().Msg("Using hungryserv, sending messages with batch send endpoint")
portal.forwardBatchSend(log, source, messages) portal.forwardBatchSend(log, source, messages, thread)
} else { } else {
log.Debug().Msg("Not using hungryserv, sending messages one by one") log.Debug().Msg("Not using hungryserv, sending messages one by one")
for _, msg := range messages { for _, msg := range messages {
portal.handleDiscordMessageCreate(source, msg, nil) portal.handleDiscordMessageCreate(source, msg, thread)
} }
} }
} }
func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, messages []*discordgo.Message) { func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, messages []*discordgo.Message, thread *Thread) {
evts, dbMessages := portal.convertMessageBatch(log, source, messages) evts, metas, dbMessages := portal.convertMessageBatch(log, source, messages, thread)
if len(evts) == 0 { if len(evts) == 0 {
log.Warn().Msg("Didn't get any events to backfill") log.Warn().Msg("Didn't get any events to backfill")
return return
} }
log.Info().Int("events", len(evts)).Msg("Converted messages to backfill") log.Info().Int("events", len(evts)).Msg("Converted messages to backfill")
resp, err := portal.MainIntent().BatchSend(portal.MXID, &mautrix.ReqBatchSend{ resp, err := portal.MainIntent().BeeperBatchSend(portal.MXID, &mautrix.ReqBeeperBatchSend{
BeeperNewMessages: true, Forward: true,
Events: evts, Events: evts,
}) })
if err != nil { if err != nil {
log.Err(err).Msg("Error sending backfill batch") log.Err(err).Msg("Error sending backfill batch")
@@ -199,25 +228,42 @@ func (portal *Portal) forwardBatchSend(log zerolog.Logger, source *User, message
} }
for i, evtID := range resp.EventIDs { for i, evtID := range resp.EventIDs {
dbMessages[i].MXID = evtID dbMessages[i].MXID = evtID
if metas[i] != nil && metas[i].Flags == discordgo.MessageFlagsHasThread {
// TODO proper context
ctx := log.WithContext(context.Background())
portal.bridge.threadFound(ctx, source, &dbMessages[i], metas[i].ID, metas[i].Thread)
}
} }
portal.bridge.DB.Message.MassInsert(portal.Key, dbMessages) 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) { func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, messages []*discordgo.Message, thread *Thread) ([]*event.Event, []*discordgo.Message, []database.Message) {
var discordThreadID string
var threadRootEvent, lastThreadEvent id.EventID
if thread != nil {
discordThreadID = thread.ID
threadRootEvent = thread.RootMXID
lastThreadEvent = threadRootEvent
lastInThread := portal.bridge.DB.Message.GetLastInThread(portal.Key, thread.ID)
if lastInThread != nil {
lastThreadEvent = lastInThread.MXID
}
}
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))
metas := make([]*discordgo.Message, 0, len(messages))
ctx := context.Background() 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)
puppet.UpdateInfo(nil, mention, "") puppet.UpdateInfo(nil, mention, nil)
} }
puppet := portal.bridge.GetPuppetByID(msg.Author.ID) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
puppet.UpdateInfo(source, msg.Author, msg.WebhookID) puppet.UpdateInfo(source, msg.Author, msg)
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
replyTo := portal.getReplyTarget(source, "", msg.MessageReference, msg.Embeds, true) replyTo := portal.getReplyTarget(source, discordThreadID, msg.MessageReference, msg.Embeds, true)
mentions := portal.convertDiscordMentions(msg, false) mentions := portal.convertDiscordMentions(msg, false)
ts, _ := discordgo.SnowflakeTimestamp(msg.ID) ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
@@ -228,8 +274,14 @@ func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, mess
Logger() Logger()
parts := portal.convertDiscordMessage(log.WithContext(ctx), puppet, intent, msg) parts := portal.convertDiscordMessage(log.WithContext(ctx), puppet, intent, msg)
for i, part := range parts { for i, part := range parts {
if (replyTo != nil || threadRootEvent != "") && part.Content.RelatesTo == nil {
part.Content.RelatesTo = &event.RelatesTo{}
}
if threadRootEvent != "" {
part.Content.RelatesTo.SetThread(threadRootEvent, lastThreadEvent)
}
if replyTo != nil { if replyTo != nil {
part.Content.RelatesTo = &event.RelatesTo{InReplyTo: replyTo} part.Content.RelatesTo.SetReplyTo(replyTo.EventID)
// Only set reply for first event // Only set reply for first event
replyTo = nil replyTo = nil
} }
@@ -270,9 +322,15 @@ func (portal *Portal) convertMessageBatch(log zerolog.Logger, source *User, mess
AttachmentID: part.AttachmentID, AttachmentID: part.AttachmentID,
SenderMXID: intent.UserID, SenderMXID: intent.UserID,
}) })
if i == 0 {
metas = append(metas, msg)
} else {
metas = append(metas, nil)
}
lastThreadEvent = evt.ID
} }
} }
return evts, dbMessages return evts, metas, dbMessages
} }
func (portal *Portal) deterministicEventID(messageID, partName string) id.EventID { func (portal *Portal) deterministicEventID(messageID, partName string) id.EventID {

View File

@@ -159,7 +159,7 @@ func fnLoginToken(ce *WrappedCommandEvent) {
ce.Reply("Error connecting to Discord: %v", err) ce.Reply("Error connecting to Discord: %v", err)
return return
} }
ce.Reply("Successfully logged in as %s#%s", ce.User.Session.State.User.Username, ce.User.Session.State.User.Discriminator) ce.Reply("Successfully logged in as @%s", ce.User.Session.State.User.Username)
} }
var cmdLoginQR = &commands.FullHandler{ var cmdLoginQR = &commands.FullHandler{
@@ -228,7 +228,7 @@ func fnLoginQR(ce *WrappedCommandEvent) {
ce.User.DiscordID = user.UserID ce.User.DiscordID = user.UserID
ce.User.Update() ce.User.Update()
ce.User.Unlock() ce.User.Unlock()
ce.Reply("Successfully logged in as %s#%s", user.Username, user.Discriminator) ce.Reply("Successfully logged in as @%s", user.Username)
} }
func sendQRCode(ce *WrappedCommandEvent, code string) id.EventID { func sendQRCode(ce *WrappedCommandEvent, code string) id.EventID {
@@ -308,7 +308,7 @@ func fnPing(ce *WrappedCommandEvent) {
} else if ce.User.wasDisconnected { } else if ce.User.wasDisconnected {
ce.Reply("You're logged in, but the Discord connection seems to be dead 💥") ce.Reply("You're logged in, but the Discord connection seems to be dead 💥")
} else { } else {
ce.Reply("You're logged in as %s#%s (`%s`)", ce.User.Session.State.User.Username, ce.User.Session.State.User.Discriminator, ce.User.DiscordID) ce.Reply("You're logged in as @%s (`%s`)", ce.User.Session.State.User.Username, ce.User.DiscordID)
} }
} }

View File

@@ -207,6 +207,7 @@ func (mp *MediaPatterns) Avatar(userID, avatarID, ext string) id.ContentURI {
type BackfillLimitPart struct { type BackfillLimitPart struct {
DM int `yaml:"dm"` DM int `yaml:"dm"`
Channel int `yaml:"channel"` Channel int `yaml:"channel"`
Thread int `yaml:"thread"`
} }
func (bc *BridgeConfig) GetResendBridgeInfo() bool { func (bc *BridgeConfig) GetResendBridgeInfo() bool {
@@ -291,12 +292,17 @@ func (bc BridgeConfig) FormatUsername(userID string) string {
type DisplaynameParams struct { type DisplaynameParams struct {
*discordgo.User *discordgo.User
Webhook bool Webhook bool
Application bool
} }
func (bc BridgeConfig) FormatDisplayname(user *discordgo.User, webhook bool) string { func (bc BridgeConfig) FormatDisplayname(user *discordgo.User, webhook, application bool) string {
var buffer strings.Builder var buffer strings.Builder
_ = bc.displaynameTemplate.Execute(&buffer, &DisplaynameParams{user, webhook}) _ = bc.displaynameTemplate.Execute(&buffer, &DisplaynameParams{
User: user,
Webhook: webhook,
Application: application,
})
return buffer.String() return buffer.String()
} }

View File

@@ -17,9 +17,9 @@
package config package config
import ( import (
up "go.mau.fi/util/configupgrade"
"go.mau.fi/util/random"
"maunium.net/go/mautrix/bridge/bridgeconfig" "maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/util"
up "maunium.net/go/mautrix/util/configupgrade"
) )
func DoUpgrade(helper *up.Helper) { func DoUpgrade(helper *up.Helper) {
@@ -79,8 +79,10 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "backfill", "enabled") helper.Copy(up.Bool, "bridge", "backfill", "enabled")
helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "initial", "dm") helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "initial", "dm")
helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "initial", "channel") helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "initial", "channel")
helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "initial", "thread")
helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "missed", "dm") helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "missed", "dm")
helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "missed", "channel") helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "missed", "channel")
helper.Copy(up.Int, "bridge", "backfill", "forward_limits", "missed", "thread")
helper.Copy(up.Int, "bridge", "backfill", "max_guild_members") helper.Copy(up.Int, "bridge", "backfill", "max_guild_members")
helper.Copy(up.Bool, "bridge", "encryption", "allow") helper.Copy(up.Bool, "bridge", "encryption", "allow")
helper.Copy(up.Bool, "bridge", "encryption", "default") helper.Copy(up.Bool, "bridge", "encryption", "default")
@@ -95,6 +97,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_prev_on_new_session") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_prev_on_new_session")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_on_device_delete") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_on_device_delete")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "periodically_delete_expired") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "periodically_delete_expired")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outdated_inbound")
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "receive") helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "receive")
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "send") helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "send")
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "share") helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "share")
@@ -105,7 +108,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Str, "bridge", "provisioning", "prefix") helper.Copy(up.Str, "bridge", "provisioning", "prefix")
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" { if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
sharedSecret := util.RandomString(64) sharedSecret := random.String(64)
helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret") helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret")
} else { } else {
helper.Copy(up.Str, "bridge", "provisioning", "shared_secret") helper.Copy(up.Str, "bridge", "provisioning", "shared_secret")

View File

@@ -5,10 +5,9 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"go.mau.fi/util/dbutil"
"maunium.net/go/maulogger/v2" "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/mautrix-discord/database/upgrades" "go.mau.fi/mautrix-discord/database/upgrades"
) )

View File

@@ -6,11 +6,10 @@ import (
"errors" "errors"
"time" "time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
type FileQuery struct { type FileQuery struct {

View File

@@ -6,10 +6,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
type GuildBridgingMode int type GuildBridgingMode int

View File

@@ -7,10 +7,9 @@ import (
"strings" "strings"
"time" "time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
type MessageQuery struct { type MessageQuery struct {

View File

@@ -4,11 +4,9 @@ import (
"database/sql" "database/sql"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
// language=postgresql // language=postgresql

View File

@@ -3,15 +3,14 @@ package database
import ( import (
"database/sql" "database/sql"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
const ( const (
puppetSelect = "SELECT id, name, name_set, avatar, avatar_url, avatar_set," + puppetSelect = "SELECT id, name, name_set, avatar, avatar_url, avatar_set," +
" contact_info_set, global_name, username, discriminator, is_bot, is_webhook, custom_mxid, access_token, next_batch" + " contact_info_set, global_name, username, discriminator, is_bot, is_webhook, is_application, custom_mxid, access_token, next_batch" +
" FROM puppet " " FROM puppet "
) )
@@ -80,6 +79,7 @@ type Puppet struct {
Discriminator string Discriminator string
IsBot bool IsBot bool
IsWebhook bool IsWebhook bool
IsApplication bool
CustomMXID id.UserID CustomMXID id.UserID
AccessToken string AccessToken string
@@ -91,7 +91,7 @@ func (p *Puppet) Scan(row dbutil.Scannable) *Puppet {
var customMXID, accessToken, nextBatch sql.NullString var customMXID, accessToken, nextBatch sql.NullString
err := row.Scan(&p.ID, &p.Name, &p.NameSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.ContactInfoSet, err := row.Scan(&p.ID, &p.Name, &p.NameSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.ContactInfoSet,
&p.GlobalName, &p.Username, &p.Discriminator, &p.IsBot, &p.IsWebhook, &customMXID, &accessToken, &nextBatch) &p.GlobalName, &p.Username, &p.Discriminator, &p.IsBot, &p.IsWebhook, &p.IsApplication, &customMXID, &accessToken, &nextBatch)
if err != nil { if err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
@@ -114,13 +114,13 @@ func (p *Puppet) Insert() {
query := ` query := `
INSERT INTO puppet ( INSERT INTO puppet (
id, name, name_set, avatar, avatar_url, avatar_set, contact_info_set, id, name, name_set, avatar, avatar_url, avatar_set, contact_info_set,
global_name, username, discriminator, is_bot, is_webhook, global_name, username, discriminator, is_bot, is_webhook, is_application,
custom_mxid, access_token, next_batch custom_mxid, access_token, next_batch
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
` `
_, err := p.db.Exec(query, p.ID, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet, _, err := p.db.Exec(query, p.ID, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet,
p.GlobalName, p.Username, p.Discriminator, p.IsBot, p.IsWebhook, p.GlobalName, p.Username, p.Discriminator, p.IsBot, p.IsWebhook, p.IsApplication,
strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch)) strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch))
if err != nil { if err != nil {
@@ -132,14 +132,14 @@ func (p *Puppet) Insert() {
func (p *Puppet) Update() { func (p *Puppet) Update() {
query := ` query := `
UPDATE puppet SET name=$1, name_set=$2, avatar=$3, avatar_url=$4, avatar_set=$5, contact_info_set=$6, UPDATE puppet SET name=$1, name_set=$2, avatar=$3, avatar_url=$4, avatar_set=$5, contact_info_set=$6,
global_name=$7, username=$8, discriminator=$9, is_bot=$10, is_webhook=$11, global_name=$7, username=$8, discriminator=$9, is_bot=$10, is_webhook=$11, is_application=$12,
custom_mxid=$12, access_token=$13, next_batch=$14 custom_mxid=$13, access_token=$14, next_batch=$15
WHERE id=$15 WHERE id=$16
` `
_, err := p.db.Exec( _, err := p.db.Exec(
query, query,
p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet, p.Name, p.NameSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.ContactInfoSet,
p.GlobalName, p.Username, p.Discriminator, p.IsBot, p.IsWebhook, p.GlobalName, p.Username, p.Discriminator, p.IsBot, p.IsWebhook, p.IsApplication,
strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch), strPtr(p.CustomMXID), strPtr(p.AccessToken), strPtr(p.NextBatch),
p.ID, p.ID,
) )

View File

@@ -4,10 +4,9 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
type ReactionQuery struct { type ReactionQuery struct {

View File

@@ -4,11 +4,9 @@ import (
"database/sql" "database/sql"
"errors" "errors"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/util/dbutil"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
) )
type RoleQuery struct { type RoleQuery struct {

View File

@@ -4,10 +4,9 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
type ThreadQuery struct { type ThreadQuery struct {

View File

@@ -1,4 +1,4 @@
-- v0 -> v22 (compatible with v19+): Latest revision -- v0 -> v23 (compatible with v19+): Latest revision
CREATE TABLE guild ( CREATE TABLE guild (
dcid TEXT PRIMARY KEY, dcid TEXT PRIMARY KEY,
@@ -71,11 +71,12 @@ CREATE TABLE puppet (
contact_info_set BOOLEAN NOT NULL DEFAULT false, contact_info_set BOOLEAN NOT NULL DEFAULT false,
global_name TEXT NOT NULL DEFAULT '', global_name TEXT NOT NULL DEFAULT '',
username TEXT NOT NULL DEFAULT '', username TEXT NOT NULL DEFAULT '',
discriminator TEXT NOT NULL DEFAULT '', discriminator TEXT NOT NULL DEFAULT '',
is_bot BOOLEAN NOT NULL DEFAULT false, is_bot BOOLEAN NOT NULL DEFAULT false,
is_webhook BOOLEAN NOT NULL DEFAULT false, is_webhook BOOLEAN NOT NULL DEFAULT false,
is_application BOOLEAN NOT NULL DEFAULT false,
custom_mxid TEXT, custom_mxid TEXT,
access_token TEXT, access_token TEXT,

View File

@@ -0,0 +1,2 @@
-- v23 (compatible with v19+): Store is application status for puppets
ALTER TABLE puppet ADD COLUMN is_application BOOLEAN NOT NULL DEFAULT false;

View File

@@ -19,7 +19,7 @@ package upgrades
import ( import (
"embed" "embed"
"maunium.net/go/mautrix/util/dbutil" "go.mau.fi/util/dbutil"
) )
var Table dbutil.UpgradeTable var Table dbutil.UpgradeTable

View File

@@ -3,10 +3,9 @@ package database
import ( import (
"database/sql" "database/sql"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
) )
type UserQuery struct { type UserQuery struct {

View File

@@ -5,8 +5,8 @@ import (
"errors" "errors"
"time" "time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/util/dbutil"
) )
const ( const (

View File

@@ -92,7 +92,8 @@ bridge:
# .Discriminator - The 4 numbers after the name on Discord # .Discriminator - The 4 numbers after the name on Discord
# .Bot - Whether the user is a bot # .Bot - Whether the user is a bot
# .System - Whether the user is an official system user # .System - Whether the user is an official system user
# .Webhook - Whether the user is a webhook # .Webhook - Whether the user is a webhook and is not an application
# .Application - Whether the user is an application
displayname_template: '{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}' displayname_template: '{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}'
# Displayname template for Discord channels (bridged as rooms, or spaces when type=4). # Displayname template for Discord channels (bridged as rooms, or spaces when type=4).
# Available variables: # Available variables:
@@ -232,6 +233,7 @@ bridge:
initial: initial:
dm: 0 dm: 0
channel: 0 channel: 0
thread: 0
# Missed message backfill (on startup). # Missed message backfill (on startup).
# 0 means backfill is disabled, -1 means fetch all messages since last bridged message. # 0 means backfill is disabled, -1 means fetch all messages since last bridged message.
# When using unlimited backfill (-1), messages are backfilled as they are fetched. # When using unlimited backfill (-1), messages are backfilled as they are fetched.
@@ -239,6 +241,7 @@ bridge:
missed: missed:
dm: 0 dm: 0
channel: 0 channel: 0
thread: 0
# Maximum members in a guild to enable backfilling. Set to -1 to disable limit. # Maximum members in a guild to enable backfilling. Set to -1 to disable limit.
# This can be used as a rough heuristic to disable backfilling in channels that are too active. # This can be used as a rough heuristic to disable backfilling in channels that are too active.
# Currently only applies to missed message backfill. # Currently only applies to missed message backfill.
@@ -279,6 +282,10 @@ bridge:
delete_on_device_delete: false delete_on_device_delete: false
# Periodically delete megolm sessions when 2x max_age has passed since receiving the session. # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.
periodically_delete_expired: false periodically_delete_expired: false
# Delete inbound megolm sessions that don't have the received_at field used for
# automatic ratcheting and expired session deletion. This is meant as a migration
# to delete old keys prior to the bridge update.
delete_outdated_inbound: false
# What level of device verification should be required from users? # What level of device verification should be required from users?
# #
# Valid levels: # Valid levels:

View File

@@ -26,13 +26,12 @@ import (
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
"go.mau.fi/util/variationselector"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/format/mdext" "maunium.net/go/mautrix/format/mdext"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/variationselector"
) )
// escapeFixer is a hacky partial fix for the difference in escaping markdown, used with escapeReplacement // escapeFixer is a hacky partial fix for the difference in escaping markdown, used with escapeReplacement
@@ -156,11 +155,14 @@ func (br *DiscordBridge) pillConverter(displayname, mxid, eventID string, ctx fo
return displayname return displayname
} }
const discordLinkPattern = `https?://[^<\p{Zs}\x{feff}]*[^"'),.:;\]\p{Zs}\x{feff}]`
// Discord links start with http:// or https://, contain at least two characters afterwards, // Discord links start with http:// or https://, contain at least two characters afterwards,
// don't contain < or whitespace anywhere, and don't end with "'),.:;] // don't contain < or whitespace anywhere, and don't end with "'),.:;]
// //
// Zero-width whitespace is mostly in the Format category and is allowed, except \uFEFF isn't for some reason // Zero-width whitespace is mostly in the Format category and is allowed, except \uFEFF isn't for some reason
var discordLinkRegex = regexp.MustCompile(`https?://[^<\p{Zs}\x{feff}]*[^"'),.:;\]\p{Zs}\x{feff}]`) var discordLinkRegex = regexp.MustCompile(discordLinkPattern)
var discordLinkRegexFull = regexp.MustCompile("^" + discordLinkPattern + "$")
var discordMarkdownEscaper = strings.NewReplacer( var discordMarkdownEscaper = strings.NewReplacer(
`\`, `\\`, `\`, `\\`,

22
go.mod
View File

@@ -1,6 +1,6 @@
module go.mau.fi/mautrix-discord module go.mau.fi/mautrix-discord
go 1.19 go 1.20
require ( require (
github.com/bwmarrin/discordgo v0.27.0 github.com/bwmarrin/discordgo v0.27.0
@@ -10,13 +10,15 @@ require (
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.17
github.com/rs/zerolog v1.29.1 github.com/rs/zerolog v1.30.0
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.4 github.com/stretchr/testify v1.8.4
github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark v1.5.5
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 go.mau.fi/util v0.0.0-20230805171708-199bf3eec776
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
golang.org/x/sync v0.3.0
maunium.net/go/maulogger/v2 v2.4.1 maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.15.3 maunium.net/go/mautrix v0.16.0
) )
require ( require (
@@ -25,17 +27,17 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/gjson v1.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
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.10.0 // indirect golang.org/x/crypto v0.12.0 // indirect
golang.org/x/net v0.11.0 // indirect golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.9.0 // indirect golang.org/x/sys v0.11.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-20230512133900-5b12693331c0 replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230804154054-72abb5417718

42
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-20230512133900-5b12693331c0 h1:ECBEbC4ruaXzcVJJ4UurkGpT/Xlm9ZnwsHiHn9gjPZw= github.com/beeper/discordgo v0.0.0-20230804154054-72abb5417718 h1:gzOFOenpzAWnsiskTmOOorrrejm2wGjSpxzQ5zgpSso=
github.com/beeper/discordgo v0.0.0-20230512133900-5b12693331c0/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA= github.com/beeper/discordgo v0.0.0-20230804154054-72abb5417718/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -25,36 +25,40 @@ github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 h1:VrxDCO/gLFHLQywGUsJzertrvt2mUEMrZPf4hEL/s18=
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -65,5 +69,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.3 h1:C9BHSUM0gYbuZmAtopuLjIcH5XHLb/ZjTEz7nN+0jN0= maunium.net/go/mautrix v0.16.0 h1:iUqCzJE2yqBC1ddAK6eAn159My8rLb4X8g4SFtQh2Dk=
maunium.net/go/mautrix v0.15.3/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE= maunium.net/go/mautrix v0.16.0/go.mod h1:XAjE9pTSGcr6vXaiNgQGiip7tddJ8FQV1a29u2QdBG4=

View File

@@ -298,7 +298,7 @@ func (guild *Guild) cleanup() {
return return
} }
intent := guild.bridge.Bot intent := guild.bridge.Bot
if guild.bridge.SpecVersions.UnstableFeatures["com.beeper.room_yeeting"] { if guild.bridge.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
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) {
guild.log.Errorfln("Failed to delete %s using hungryserv yeet endpoint: %v", guild.MXID, err) guild.log.Errorfln("Failed to delete %s using hungryserv yeet endpoint: %v", guild.MXID, err)

13
main.go
View File

@@ -20,11 +20,12 @@ import (
_ "embed" _ "embed"
"sync" "sync"
"go.mau.fi/util/configupgrade"
"go.mau.fi/util/exsync"
"golang.org/x/sync/semaphore"
"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"
"maunium.net/go/mautrix/util"
"maunium.net/go/mautrix/util/configupgrade"
"go.mau.fi/mautrix-discord/config" "go.mau.fi/mautrix-discord/config"
"go.mau.fi/mautrix-discord/database" "go.mau.fi/mautrix-discord/database"
@@ -73,7 +74,8 @@ type DiscordBridge struct {
puppetsByCustomMXID map[id.UserID]*Puppet puppetsByCustomMXID map[id.UserID]*Puppet
puppetsLock sync.Mutex puppetsLock sync.Mutex
attachmentTransfers *util.SyncMap[attachmentKey, *util.ReturnableOnce[*database.File]] attachmentTransfers *exsync.Map[attachmentKey, *exsync.ReturnableOnce[*database.File]]
parallelAttachmentSemaphore *semaphore.Weighted
} }
func (br *DiscordBridge) GetExampleConfig() string { func (br *DiscordBridge) GetExampleConfig() string {
@@ -170,13 +172,14 @@ func main() {
puppets: make(map[string]*Puppet), puppets: make(map[string]*Puppet),
puppetsByCustomMXID: make(map[id.UserID]*Puppet), puppetsByCustomMXID: make(map[id.UserID]*Puppet),
attachmentTransfers: util.NewSyncMap[attachmentKey, *util.ReturnableOnce[*database.File]](), attachmentTransfers: exsync.NewMap[attachmentKey, *exsync.ReturnableOnce[*database.File]](),
parallelAttachmentSemaphore: semaphore.NewWeighted(3),
} }
br.Bridge = bridge.Bridge{ br.Bridge = bridge.Bridge{
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.5.0", Version: "0.6.1",
ProtocolName: "Discord", ProtocolName: "Discord",
BeeperServiceName: "discordgo", BeeperServiceName: "discordgo",
BeeperNetworkName: "discord", BeeperNetworkName: "discord",

View File

@@ -18,6 +18,8 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/util/variationselector"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge"
@@ -26,8 +28,6 @@ import (
"maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util"
"maunium.net/go/mautrix/util/variationselector"
"go.mau.fi/mautrix-discord/config" "go.mau.fi/mautrix-discord/config"
"go.mau.fi/mautrix-discord/database" "go.mau.fi/mautrix-discord/database"
@@ -62,7 +62,7 @@ type Portal struct {
discordMessages chan portalDiscordMessage discordMessages chan portalDiscordMessage
matrixMessages chan portalMatrixMessage matrixMessages chan portalMatrixMessage
recentMessages *util.RingBuffer[string, *discordgo.Message] recentMessages *exsync.RingBuffer[string, *discordgo.Message]
commands map[string]*discordgo.ApplicationCommand commands map[string]*discordgo.ApplicationCommand
commandsLock sync.RWMutex commandsLock sync.RWMutex
@@ -260,7 +260,7 @@ func (br *DiscordBridge) NewPortal(dbPortal *database.Portal) *Portal {
discordMessages: make(chan portalDiscordMessage, br.Config.Bridge.PortalMessageBuffer), discordMessages: make(chan portalDiscordMessage, br.Config.Bridge.PortalMessageBuffer),
matrixMessages: make(chan portalMatrixMessage, br.Config.Bridge.PortalMessageBuffer), matrixMessages: make(chan portalMatrixMessage, br.Config.Bridge.PortalMessageBuffer),
recentMessages: util.NewRingBuffer[string, *discordgo.Message](recentMessageBufferSize), recentMessages: exsync.NewRingBuffer[string, *discordgo.Message](recentMessageBufferSize),
commands: make(map[string]*discordgo.ApplicationCommand), commands: make(map[string]*discordgo.ApplicationCommand),
} }
@@ -541,7 +541,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, channel *discordgo.Channel) e
portal.Update() portal.Update()
} }
go portal.forwardBackfillInitial(user) go portal.forwardBackfillInitial(user, nil)
backfillStarted = true backfillStarted = true
return nil return nil
@@ -549,13 +549,15 @@ func (portal *Portal) CreateMatrixRoom(user *User, channel *discordgo.Channel) e
func (portal *Portal) handleDiscordMessages(msg portalDiscordMessage) { func (portal *Portal) handleDiscordMessages(msg portalDiscordMessage) {
if portal.MXID == "" { if portal.MXID == "" {
_, ok := msg.msg.(*discordgo.MessageCreate) msgCreate, ok := msg.msg.(*discordgo.MessageCreate)
if !ok { if !ok {
portal.log.Warn().Msg("Can't create Matrix room from non new message event") portal.log.Warn().Msg("Can't create Matrix room from non new message event")
return return
} }
portal.log.Debug().Msg("Creating Matrix room from incoming message") portal.log.Debug().
Str("message_id", msgCreate.ID).
Msg("Creating Matrix room from incoming message")
if err := portal.CreateMatrixRoom(msg.user, nil); err != nil { if err := portal.CreateMatrixRoom(msg.user, nil); err != nil {
portal.log.Err(err).Msg("Failed to create portal room") portal.log.Err(err).Msg("Failed to create portal room")
return return
@@ -586,7 +588,7 @@ func (portal *Portal) ensureUserInvited(user *User, ignoreCache bool) bool {
return user.ensureInvited(portal.MainIntent(), portal.MXID, portal.IsPrivateChat(), ignoreCache) return user.ensureInvited(portal.MainIntent(), portal.MXID, portal.IsPrivateChat(), ignoreCache)
} }
func (portal *Portal) markMessageHandled(discordID string, authorID string, timestamp time.Time, threadID string, senderMXID id.UserID, parts []database.MessagePart) { func (portal *Portal) markMessageHandled(discordID string, authorID string, timestamp time.Time, threadID string, senderMXID id.UserID, parts []database.MessagePart) *database.Message {
msg := portal.bridge.DB.Message.New() msg := portal.bridge.DB.Message.New()
msg.Channel = portal.Key msg.Channel = portal.Key
msg.DiscordID = discordID msg.DiscordID = discordID
@@ -595,6 +597,9 @@ func (portal *Portal) markMessageHandled(discordID string, authorID string, time
msg.ThreadID = threadID msg.ThreadID = threadID
msg.SenderMXID = senderMXID msg.SenderMXID = senderMXID
msg.MassInsertParts(parts) msg.MassInsertParts(parts)
msg.MXID = parts[0].MXID
msg.AttachmentID = parts[0].AttachmentID
return msg
} }
func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message, thread *Thread) { func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message, thread *Thread) {
@@ -619,10 +624,10 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
log.Debug().Msg("Dropping duplicate message") log.Debug().Msg("Dropping duplicate message")
return return
} }
log.Debug().Msg("Starting handling of Discord message")
handlingStartTime := time.Now()
puppet := portal.bridge.GetPuppetByID(msg.Author.ID) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
puppet.UpdateInfo(user, msg.Author, msg.WebhookID) puppet.UpdateInfo(user, msg.Author, msg)
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
var discordThreadID string var discordThreadID string
@@ -642,6 +647,7 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
ts, _ := discordgo.SnowflakeTimestamp(msg.ID) ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
parts := portal.convertDiscordMessage(ctx, puppet, intent, msg) parts := portal.convertDiscordMessage(ctx, puppet, intent, msg)
dbParts := make([]database.MessagePart, 0, len(parts)) dbParts := make([]database.MessagePart, 0, len(parts))
eventIDs := zerolog.Dict()
for i, part := range parts { for i, part := range parts {
if (replyTo != nil || threadRootEvent != "") && part.Content.RelatesTo == nil { if (replyTo != nil || threadRootEvent != "") && part.Content.RelatesTo == nil {
part.Content.RelatesTo = &event.RelatesTo{} part.Content.RelatesTo = &event.RelatesTo{}
@@ -672,13 +678,20 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
} }
lastThreadEvent = resp.EventID lastThreadEvent = resp.EventID
dbParts = append(dbParts, database.MessagePart{AttachmentID: part.AttachmentID, MXID: resp.EventID}) dbParts = append(dbParts, database.MessagePart{AttachmentID: part.AttachmentID, MXID: resp.EventID})
eventIDs.Str(part.AttachmentID, resp.EventID.String())
} }
log = log.With().Dur("handling_time", time.Since(handlingStartTime)).Logger()
if len(parts) == 0 { if len(parts) == 0 {
log.Warn().Msg("Unhandled message") log.Warn().Msg("Unhandled message")
} else if len(dbParts) == 0 { } else if len(dbParts) == 0 {
log.Warn().Msg("All parts of message failed to send to Matrix") log.Warn().Msg("All parts of message failed to send to Matrix")
} else { } else {
portal.markMessageHandled(msg.ID, msg.Author.ID, ts, discordThreadID, intent.UserID, dbParts) log.Debug().Dict("event_ids", eventIDs).Msg("Finished handling Discord message")
firstDBMessage := portal.markMessageHandled(msg.ID, msg.Author.ID, ts, discordThreadID, intent.UserID, dbParts)
if msg.Flags == discordgo.MessageFlagsHasThread {
portal.bridge.threadFound(ctx, user, firstDBMessage, msg.ID, msg.Thread)
}
} }
} }
@@ -702,12 +715,8 @@ func (portal *Portal) getReplyTarget(source *User, threadID string, ref *discord
if ref == nil { if ref == nil {
return nil return nil
} }
isHungry := portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry
if !isHungry {
allowNonExistent = false
}
// TODO add config option for cross-room replies // TODO add config option for cross-room replies
crossRoomReplies := isHungry crossRoomReplies := portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry
targetPortal := portal targetPortal := portal
if ref.ChannelID != portal.Key.ChannelID && ref.ChannelID != threadID && crossRoomReplies { if ref.ChannelID != portal.Key.ChannelID && ref.ChannelID != threadID && crossRoomReplies {
@@ -807,11 +816,7 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
} }
if msg.Flags == discordgo.MessageFlagsHasThread { if msg.Flags == discordgo.MessageFlagsHasThread {
thread := portal.bridge.GetThreadByID(msg.ID, existing[0]) portal.bridge.threadFound(ctx, user, existing[0], msg.ID, msg.Thread)
log.Debug().Msg("Marked message as thread root")
if thread.CreationNoticeMXID == "" {
portal.sendThreadCreationNotice(ctx, thread)
}
} }
if msg.Author == nil { if msg.Author == nil {
@@ -849,6 +854,7 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
puppet := portal.bridge.GetPuppetByID(msg.Author.ID) puppet := portal.bridge.GetPuppetByID(msg.Author.ID)
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)
redactions := zerolog.Dict()
attachmentMap := map[string]*database.Message{} attachmentMap := map[string]*database.Message{}
for _, existingPart := range existing { for _, existingPart := range existing {
if existingPart.AttachmentID != "" { if existingPart.AttachmentID != "" {
@@ -876,13 +882,14 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
} }
} }
for _, deletedAttachment := range attachmentMap { for _, deletedAttachment := range attachmentMap {
_, err := intent.RedactEvent(portal.MXID, deletedAttachment.MXID) resp, err := intent.RedactEvent(portal.MXID, deletedAttachment.MXID)
if err != nil { if err != nil {
log.Warn().Err(err). log.Warn().Err(err).
Str("event_id", deletedAttachment.MXID.String()). Str("event_id", deletedAttachment.MXID.String()).
Msg("Failed to redact attachment") Msg("Failed to redact attachment")
} }
deletedAttachment.Delete() deletedAttachment.Delete()
redactions.Str(deletedAttachment.AttachmentID, resp.EventID.String())
} }
var converted *ConvertedMessage var converted *ConvertedMessage
@@ -928,6 +935,10 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
if msg.EditedTimestamp != nil { if msg.EditedTimestamp != nil {
existing[0].UpdateEditTimestamp(*msg.EditedTimestamp) existing[0].UpdateEditTimestamp(*msg.EditedTimestamp)
} }
log.Debug().
Str("event_id", resp.EventID.String()).
Dict("redacted_attachments", redactions).
Msg("Finished handling Discord edit")
} }
func (portal *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Message) { func (portal *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Message) {
@@ -992,7 +1003,7 @@ func (portal *Portal) handleDiscordTyping(evt *discordgo.TypingStart) {
func (portal *Portal) syncParticipant(source *User, participant *discordgo.User, remove bool) { func (portal *Portal) syncParticipant(source *User, participant *discordgo.User, remove bool) {
puppet := portal.bridge.GetPuppetByID(participant.ID) puppet := portal.bridge.GetPuppetByID(participant.ID)
puppet.UpdateInfo(source, participant, "") puppet.UpdateInfo(source, participant, nil)
log := portal.log.With(). log := portal.log.With().
Str("participant_id", participant.ID). Str("participant_id", participant.ID).
Str("ghost_mxid", puppet.MXID.String()). Str("ghost_mxid", puppet.MXID.String()).
@@ -1019,7 +1030,7 @@ func (portal *Portal) syncParticipant(source *User, participant *discordgo.User,
func (portal *Portal) syncParticipants(source *User, participants []*discordgo.User) { func (portal *Portal) syncParticipants(source *User, participants []*discordgo.User) {
for _, participant := range participants { for _, participant := range participants {
puppet := portal.bridge.GetPuppetByID(participant.ID) puppet := portal.bridge.GetPuppetByID(participant.ID)
puppet.UpdateInfo(source, participant, "") puppet.UpdateInfo(source, participant, nil)
user := portal.bridge.GetUserByID(participant.ID) user := portal.bridge.GetUserByID(participant.ID)
if user != nil { if user != nil {
@@ -1168,6 +1179,9 @@ func (portal *Portal) sendErrorMessage(msgType, message string, confirmed bool)
if confirmed { if confirmed {
certainty = "was not" certainty = "was not"
} }
if portal.RelayWebhookSecret != "" {
message = strings.ReplaceAll(message, portal.RelayWebhookSecret, "<redacted>")
}
resp, err := portal.sendMatrixMessage(portal.MainIntent(), event.EventMessage, &event.MessageEventContent{ resp, err := portal.sendMatrixMessage(portal.MainIntent(), event.EventMessage, &event.MessageEventContent{
MsgType: event.MsgNotice, MsgType: event.MsgNotice,
Body: fmt.Sprintf("\u26a0 Your %s %s bridged: %v", msgType, certainty, message), Body: fmt.Sprintf("\u26a0 Your %s %s bridged: %v", msgType, certainty, message),
@@ -1463,9 +1477,10 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
} }
return return
} else if threadRoot := content.GetRelatesTo().GetThreadParent(); threadRoot != "" { } else if threadRoot := content.GetRelatesTo().GetThreadParent(); threadRoot != "" {
existingThread := portal.bridge.DB.Thread.GetByMatrixRootMsg(threadRoot) existingThread := portal.bridge.GetThreadByRootMXID(threadRoot)
if existingThread != nil { if existingThread != nil {
threadID = existingThread.ID threadID = existingThread.ID
existingThread.initialBackfillAttempted = true
} else { } else {
if isWebhookSend { if isWebhookSend {
// TODO start thread with bot? // TODO start thread with bot?
@@ -1518,6 +1533,9 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
switch content.MsgType { switch content.MsgType {
case event.MsgText, event.MsgEmote, event.MsgNotice: case event.MsgText, event.MsgEmote, event.MsgNotice:
sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content) sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content)
if content.MsgType == event.MsgEmote {
sendReq.Content = fmt.Sprintf("_%s_", sendReq.Content)
}
case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo: case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo:
data, err := downloadMatrixAttachment(portal.MainIntent(), content) data, err := downloadMatrixAttachment(portal.MainIntent(), content)
if err != nil { if err != nil {
@@ -1700,7 +1718,7 @@ func (portal *Portal) cleanup(puppetsOnly bool) {
return return
} }
intent := portal.MainIntent() intent := portal.MainIntent()
if portal.bridge.SpecVersions.UnstableFeatures["com.beeper.room_yeeting"] { if portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
err := intent.BeeperDeleteRoom(portal.MXID) err := intent.BeeperDeleteRoom(portal.MXID)
if err != nil && !errors.Is(err, mautrix.MNotFound) { if err != nil && !errors.Is(err, mautrix.MNotFound) {
portal.log.Err(err).Msg("Failed to delete room using hungryserv yeet endpoint") portal.log.Err(err).Msg("Failed to delete room using hungryserv yeet endpoint")
@@ -1848,7 +1866,7 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) {
func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageReaction, add bool, thread *Thread, member *discordgo.Member) { func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageReaction, add bool, thread *Thread, member *discordgo.Member) {
puppet := portal.bridge.GetPuppetByID(reaction.UserID) puppet := portal.bridge.GetPuppetByID(reaction.UserID)
if member != nil { if member != nil {
puppet.UpdateInfo(user, member.User, "") puppet.UpdateInfo(user, member.User, nil)
} }
intent := puppet.IntentFor(portal) intent := puppet.IntentFor(portal)

View File

@@ -82,6 +82,9 @@ func (portal *Portal) convertDiscordFile(ctx context.Context, typeName string, i
} }
func (portal *Portal) cleanupConvertedStickerInfo(content *event.MessageEventContent) { func (portal *Portal) cleanupConvertedStickerInfo(content *event.MessageEventContent) {
if content.Info == nil {
return
}
if content.Info.Width == 0 && content.Info.Height == 0 { if content.Info.Width == 0 && content.Info.Height == 0 {
content.Info.Width = DiscordStickerSize content.Info.Width = DiscordStickerSize
content.Info.Height = DiscordStickerSize content.Info.Height = DiscordStickerSize
@@ -308,6 +311,12 @@ func (portal *Portal) convertDiscordMessage(ctx context.Context, puppet *Puppet,
parts = append(parts, part) parts = append(parts, part)
} }
} }
if len(parts) == 0 && msg.Thread != nil {
parts = append(parts, &ConvertedMessage{Type: event.EventMessage, Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: fmt.Sprintf("Created a thread: %s", msg.Thread.Name),
}})
}
for _, part := range parts { for _, part := range parts {
puppet.addWebhookMeta(part, msg) puppet.addWebhookMeta(part, msg)
puppet.addMemberMeta(part, msg) puppet.addMemberMeta(part, msg)
@@ -323,24 +332,20 @@ func (puppet *Puppet) addMemberMeta(part *ConvertedMessage, msg *discordgo.Messa
part.Extra = make(map[string]any) part.Extra = make(map[string]any)
} }
var avatarURL id.ContentURI var avatarURL id.ContentURI
var discordAvatarURL string
if msg.Member.Avatar != "" { if msg.Member.Avatar != "" {
var err error var err error
avatarURL, err = puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), msg.GuildID, msg.Author.ID, msg.Author.Avatar) avatarURL, discordAvatarURL, err = puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), msg.GuildID, msg.Author.ID, msg.Author.Avatar)
if err != nil { if err != nil {
puppet.log.Warn().Err(err). puppet.log.Warn().Err(err).
Str("avatar_id", msg.Author.Avatar). Str("avatar_id", msg.Author.Avatar).
Msg("Failed to reupload guild user avatar") Msg("Failed to reupload guild user avatar")
} }
} }
var discordAvararURL string
if msg.Member.Avatar != "" {
msg.Member.User = msg.Author
discordAvararURL = msg.Member.AvatarURL("")
}
part.Extra["fi.mau.discord.guild_member_metadata"] = map[string]any{ part.Extra["fi.mau.discord.guild_member_metadata"] = map[string]any{
"nick": msg.Member.Nick, "nick": msg.Member.Nick,
"avatar_id": msg.Member.Avatar, "avatar_id": msg.Member.Avatar,
"avatar_url": discordAvararURL, "avatar_url": discordAvatarURL,
"avatar_mxc": avatarURL.String(), "avatar_mxc": avatarURL.String(),
} }
if msg.Member.Nick != "" || !avatarURL.IsEmpty() { if msg.Member.Nick != "" || !avatarURL.IsEmpty() {
@@ -370,7 +375,7 @@ func (puppet *Puppet) addWebhookMeta(part *ConvertedMessage, msg *discordgo.Mess
var avatarURL id.ContentURI var avatarURL id.ContentURI
if msg.Author.Avatar != "" { if msg.Author.Avatar != "" {
var err error var err error
avatarURL, err = puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), "", msg.Author.ID, msg.Author.Avatar) avatarURL, _, err = puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), "", msg.Author.ID, msg.Author.Avatar)
if err != nil { if err != nil {
puppet.log.Warn().Err(err). puppet.log.Warn().Err(err).
Str("avatar_id", msg.Author.Avatar). Str("avatar_id", msg.Author.Avatar).
@@ -616,9 +621,14 @@ func getEmbedType(msg *discordgo.Message, embed *discordgo.MessageEmbed) BridgeE
} }
func isPlainGifMessage(msg *discordgo.Message) bool { func isPlainGifMessage(msg *discordgo.Message) bool {
return len(msg.Embeds) == 1 && msg.Embeds[0].URL == msg.Content && if len(msg.Embeds) != 1 {
((msg.Embeds[0].Type == discordgo.EmbedTypeGifv && msg.Embeds[0].Video != nil) || return false
(msg.Embeds[0].Type == discordgo.EmbedTypeImage && msg.Embeds[0].Image == nil && msg.Embeds[0].Thumbnail != nil)) }
embed := msg.Embeds[0]
isGifVideo := embed.Type == discordgo.EmbedTypeGifv && embed.Video != nil
isGifImage := embed.Type == discordgo.EmbedTypeImage && embed.Image == nil && embed.Thumbnail != nil
contentIsOnlyURL := msg.Content == embed.URL || discordLinkRegexFull.MatchString(msg.Content)
return contentIsOnlyURL && (isGifVideo || isGifImage)
} }
func (portal *Portal) convertDiscordMentions(msg *discordgo.Message, syncGhosts bool) *event.Mentions { func (portal *Portal) convertDiscordMentions(msg *discordgo.Message, syncGhosts bool) *event.Mentions {
@@ -626,7 +636,7 @@ func (portal *Portal) convertDiscordMentions(msg *discordgo.Message, syncGhosts
for _, mention := range msg.Mentions { for _, mention := range msg.Mentions {
puppet := portal.bridge.GetPuppetByID(mention.ID) puppet := portal.bridge.GetPuppetByID(mention.ID)
if syncGhosts { if syncGhosts {
puppet.UpdateInfo(nil, mention, "") puppet.UpdateInfo(nil, mention, nil)
} }
user := portal.bridge.GetUserByID(mention.ID) user := portal.bridge.GetUserByID(mention.ID)
if user != nil { if user != nil {
@@ -659,7 +669,7 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
var htmlParts []string var htmlParts []string
if msg.Interaction != nil { if msg.Interaction != nil {
puppet := portal.bridge.GetPuppetByID(msg.Interaction.User.ID) puppet := portal.bridge.GetPuppetByID(msg.Interaction.User.ID)
puppet.UpdateInfo(nil, msg.Interaction.User, "") puppet.UpdateInfo(nil, msg.Interaction.User, nil)
htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, puppet.MXID, puppet.Name, msg.Interaction.Name)) htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, puppet.MXID, puppet.Name, msg.Interaction.Name))
} }
if msg.Content != "" && !isPlainGifMessage(msg) { if msg.Content != "" && !isPlainGifMessage(msg) {
@@ -706,7 +716,7 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
"com.beeper.linkpreviews": previews, "com.beeper.linkpreviews": previews,
} }
if msg.WebhookID != "" && portal.bridge.Config.Bridge.PrefixWebhookMessages { if msg.WebhookID != "" && msg.ApplicationID == "" && portal.bridge.Config.Bridge.PrefixWebhookMessages {
content.EnsureHasHTML() content.EnsureHasHTML()
content.Body = fmt.Sprintf("%s: %s", msg.Author.Username, content.Body) content.Body = fmt.Sprintf("%s: %s", msg.Author.Username, content.Body)
content.FormattedBody = fmt.Sprintf("<strong>%s</strong>: %s", html.EscapeString(msg.Author.Username), content.FormattedBody) content.FormattedBody = fmt.Sprintf("<strong>%s</strong>: %s", html.EscapeString(msg.Author.Username), content.FormattedBody)

View File

@@ -9,9 +9,9 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"maunium.net/go/mautrix"
"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"
@@ -195,7 +195,7 @@ func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) {
} }
func (puppet *Puppet) UpdateName(info *discordgo.User) bool { func (puppet *Puppet) UpdateName(info *discordgo.User) bool {
newName := puppet.bridge.Config.Bridge.FormatDisplayname(info, puppet.IsWebhook) newName := puppet.bridge.Config.Bridge.FormatDisplayname(info, puppet.IsWebhook, puppet.IsApplication)
if puppet.Name == newName && puppet.NameSet { if puppet.Name == newName && puppet.NameSet {
return false return false
} }
@@ -216,7 +216,7 @@ func (puppet *Puppet) UpdateName(info *discordgo.User) bool {
return true return true
} }
func (br *DiscordBridge) reuploadUserAvatar(intent *appservice.IntentAPI, guildID, userID, avatarID string) (id.ContentURI, error) { func (br *DiscordBridge) reuploadUserAvatar(intent *appservice.IntentAPI, guildID, userID, avatarID string) (id.ContentURI, string, error) {
var downloadURL, ext string var downloadURL, ext string
if guildID == "" { if guildID == "" {
downloadURL = discordgo.EndpointUserAvatar(userID, avatarID) downloadURL = discordgo.EndpointUserAvatar(userID, avatarID)
@@ -233,17 +233,19 @@ func (br *DiscordBridge) reuploadUserAvatar(intent *appservice.IntentAPI, guildI
ext = "gif" ext = "gif"
} }
} }
url := br.Config.Bridge.MediaPatterns.Avatar(userID, avatarID, ext) if guildID == "" {
if !url.IsEmpty() { url := br.Config.Bridge.MediaPatterns.Avatar(userID, avatarID, ext)
return url, nil if !url.IsEmpty() {
return url, downloadURL, nil
}
} }
copied, err := br.copyAttachmentToMatrix(intent, downloadURL, false, AttachmentMeta{ copied, err := br.copyAttachmentToMatrix(intent, downloadURL, false, AttachmentMeta{
AttachmentID: fmt.Sprintf("avatar/%s/%s/%s", guildID, userID, avatarID), AttachmentID: fmt.Sprintf("avatar/%s/%s/%s", guildID, userID, avatarID),
}) })
if err != nil { if err != nil {
return url, err return id.ContentURI{}, downloadURL, err
} }
return copied.MXC, nil return copied.MXC, downloadURL, nil
} }
func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool { func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
@@ -260,7 +262,7 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
puppet.AvatarURL = id.ContentURI{} puppet.AvatarURL = id.ContentURI{}
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) { if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
url, err := puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), "", info.ID, puppet.Avatar) url, _, err := puppet.bridge.reuploadUserAvatar(puppet.DefaultIntent(), "", info.ID, puppet.Avatar)
if err != nil { if err != nil {
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar") puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
return true return true
@@ -283,7 +285,7 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
return true return true
} }
func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User, webhookID string) { func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User, message *discordgo.Message) {
puppet.syncLock.Lock() puppet.syncLock.Lock()
defer puppet.syncLock.Unlock() defer puppet.syncLock.Unlock()
@@ -306,9 +308,24 @@ func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User, webhookID s
} }
changed := false changed := false
if webhookID != "" && webhookID == info.ID && !puppet.IsWebhook { if message != nil {
puppet.IsWebhook = true if message.WebhookID != "" && message.ApplicationID == "" && !puppet.IsWebhook {
changed = true puppet.log.Debug().
Str("message_id", message.ID).
Str("webhook_id", message.WebhookID).
Msg("Found webhook ID in message, marking ghost as a webhook")
puppet.IsWebhook = true
changed = true
}
if message.ApplicationID != "" && !puppet.IsApplication {
puppet.log.Debug().
Str("message_id", message.ID).
Str("application_id", message.ApplicationID).
Msg("Found application ID in message, marking ghost as an application")
puppet.IsApplication = true
puppet.IsWebhook = false
changed = true
}
} }
changed = puppet.UpdateContactInfo(info) || changed changed = puppet.UpdateContactInfo(info) || changed
changed = puppet.UpdateName(info) || changed changed = puppet.UpdateName(info) || changed
@@ -345,12 +362,16 @@ func (puppet *Puppet) UpdateContactInfo(info *discordgo.User) bool {
} }
func (puppet *Puppet) ResendContactInfo() { func (puppet *Puppet) ResendContactInfo() {
if puppet.bridge.Config.Homeserver.Software != bridgeconfig.SoftwareHungry || puppet.ContactInfoSet { if !puppet.bridge.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) || puppet.ContactInfoSet {
return return
} }
discordUsername := puppet.Username
if puppet.Discriminator != "0" {
discordUsername += "#" + puppet.Discriminator
}
contactInfo := map[string]any{ contactInfo := map[string]any{
"com.beeper.bridge.identifiers": []string{ "com.beeper.bridge.identifiers": []string{
fmt.Sprintf("discord:%s#%s", puppet.Username, puppet.Discriminator), fmt.Sprintf("discord:%s", discordUsername),
}, },
"com.beeper.bridge.remote_id": puppet.ID, "com.beeper.bridge.remote_id": puppet.ID,
"com.beeper.bridge.service": puppet.bridge.BeeperServiceName, "com.beeper.bridge.service": puppet.bridge.BeeperServiceName,

View File

@@ -1,10 +1,13 @@
package main package main
import ( import (
"context"
"sync" "sync"
"time" "time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database" "go.mau.fi/mautrix-discord/database"
@@ -14,7 +17,8 @@ type Thread struct {
*database.Thread *database.Thread
Parent *Portal Parent *Portal
creationNoticeLock sync.Mutex creationNoticeLock sync.Mutex
initialBackfillAttempted bool
} }
func (br *DiscordBridge) GetThreadByID(id string, root *database.Message) *Thread { func (br *DiscordBridge) GetThreadByID(id string, root *database.Message) *Thread {
@@ -74,12 +78,63 @@ func (br *DiscordBridge) loadThread(dbThread *database.Thread, id string, root *
return thread return thread
} }
func (br *DiscordBridge) threadFound(ctx context.Context, source *User, rootMessage *database.Message, id string, metadata *discordgo.Channel) {
thread := br.GetThreadByID(id, rootMessage)
log := zerolog.Ctx(ctx)
log.Debug().Msg("Marked message as thread root")
if thread.CreationNoticeMXID == "" {
thread.Parent.sendThreadCreationNotice(ctx, thread)
}
// TODO member_ids_preview is probably not guaranteed to contain the source user
if source != nil && metadata != nil && slices.Contains(metadata.MemberIDsPreview, source.DiscordID) && !source.IsInPortal(thread.ID) {
source.MarkInPortal(database.UserPortal{
DiscordID: thread.ID,
Type: database.UserPortalTypeThread,
Timestamp: time.Now(),
})
if metadata.MessageCount > 0 {
go thread.maybeInitialBackfill(source)
} else {
thread.initialBackfillAttempted = true
}
}
}
func (thread *Thread) maybeInitialBackfill(source *User) {
if thread.initialBackfillAttempted || thread.Parent.bridge.Config.Bridge.Backfill.Limits.Initial.Thread == 0 {
return
}
thread.Parent.forwardBackfillLock.Lock()
if thread.Parent.bridge.DB.Message.GetLastInThread(thread.Parent.Key, thread.ID) != nil {
thread.Parent.forwardBackfillLock.Unlock()
return
}
thread.Parent.forwardBackfillInitial(source, thread)
}
func (thread *Thread) Join(user *User) { func (thread *Thread) Join(user *User) {
if user.IsInPortal(thread.ID) { if user.IsInPortal(thread.ID) {
return return
} }
log := user.log.With().Str("thread_id", thread.ID).Str("channel_id", thread.ParentID).Logger() log := user.log.With().Str("thread_id", thread.ID).Str("channel_id", thread.ParentID).Logger()
log.Debug().Msg("Joining thread") log.Debug().Msg("Joining thread")
var doBackfill, backfillStarted bool
if !thread.initialBackfillAttempted && thread.Parent.bridge.Config.Bridge.Backfill.Limits.Initial.Thread > 0 {
thread.Parent.forwardBackfillLock.Lock()
lastMessage := thread.Parent.bridge.DB.Message.GetLastInThread(thread.Parent.Key, thread.ID)
if lastMessage != nil {
thread.Parent.forwardBackfillLock.Unlock()
} else {
doBackfill = true
defer func() {
if !backfillStarted {
thread.Parent.forwardBackfillLock.Unlock()
}
}()
}
}
var err error var err error
if user.Session.IsUser { if user.Session.IsUser {
err = user.Session.ThreadJoinWithLocation(thread.ID, discordgo.ThreadJoinLocationContextMenu) err = user.Session.ThreadJoinWithLocation(thread.ID, discordgo.ThreadJoinLocationContextMenu)
@@ -94,5 +149,9 @@ func (thread *Thread) Join(user *User) {
Type: database.UserPortalTypeThread, Type: database.UserPortalTypeThread,
Timestamp: time.Now(), Timestamp: time.Now(),
}) })
if doBackfill {
go thread.Parent.forwardBackfillInitial(user, thread)
backfillStarted = true
}
} }
} }

60
user.go
View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
@@ -16,8 +17,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" "go.mau.fi/util/dbutil"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge"
@@ -73,6 +73,9 @@ func (user *User) GetRemoteID() string {
func (user *User) GetRemoteName() string { func (user *User) GetRemoteName() string {
if user.Session != nil && user.Session.State != nil && user.Session.State.User != nil { if user.Session != nil && user.Session.State != nil && user.Session.State.User != nil {
if user.Session.State.User.Discriminator == "0" {
return fmt.Sprintf("@%s", user.Session.State.User.Username)
}
return fmt.Sprintf("%s#%s", user.Session.State.User.Username, user.Session.State.User.Discriminator) return fmt.Sprintf("%s#%s", user.Session.State.User.Username, user.Session.State.User.Discriminator)
} }
return user.DiscordID return user.DiscordID
@@ -650,6 +653,8 @@ func (user *User) eventHandler(rawEvt any) {
user.typingStartHandler(evt) user.typingStartHandler(evt)
case *discordgo.InteractionSuccess: case *discordgo.InteractionSuccess:
user.interactionSuccessHandler(evt) user.interactionSuccessHandler(evt)
case *discordgo.ThreadListSync:
user.threadListSyncHandler(evt)
case *discordgo.Event: case *discordgo.Event:
// Ignore // Ignore
default: default:
@@ -864,7 +869,7 @@ func (user *User) handlePrivateChannel(portal *Portal, meta *discordgo.Channel,
} }
} else { } else {
portal.UpdateInfo(user, meta) portal.UpdateInfo(user, meta)
portal.ForwardBackfillMissed(user, meta) portal.ForwardBackfillMissed(user, meta.LastMessageID, nil)
} }
user.MarkInPortal(database.UserPortal{ user.MarkInPortal(database.UserPortal{
DiscordID: portal.Key.ChannelID, DiscordID: portal.Key.ChannelID,
@@ -954,8 +959,11 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp
guild.UpdateInfo(user, meta) guild.UpdateInfo(user, meta)
if len(meta.Channels) > 0 { if len(meta.Channels) > 0 {
for _, ch := range meta.Channels { for _, ch := range meta.Channels {
if !user.channelIsBridgeable(ch) {
continue
}
portal := user.GetPortalByMeta(ch) portal := user.GetPortalByMeta(ch)
if guild.BridgingMode >= database.GuildBridgeEverything && portal.MXID == "" && user.channelIsBridgeable(ch) { if guild.BridgingMode >= database.GuildBridgeEverything && portal.MXID == "" {
err := portal.CreateMatrixRoom(user, ch) err := portal.CreateMatrixRoom(user, ch)
if err != nil { if err != nil {
user.log.Error().Err(err). user.log.Error().Err(err).
@@ -966,7 +974,7 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp
} else { } else {
portal.UpdateInfo(user, ch) portal.UpdateInfo(user, ch)
if user.bridge.Config.Bridge.Backfill.MaxGuildMembers < 0 || meta.MemberCount < user.bridge.Config.Bridge.Backfill.MaxGuildMembers { if user.bridge.Config.Bridge.Backfill.MaxGuildMembers < 0 || meta.MemberCount < user.bridge.Config.Bridge.Backfill.MaxGuildMembers {
portal.ForwardBackfillMissed(user, ch) portal.ForwardBackfillMissed(user, ch.LastMessageID, nil)
} }
} }
} }
@@ -1018,6 +1026,10 @@ func (user *User) guildCreateHandler(g *discordgo.GuildCreate) {
} }
func (user *User) guildDeleteHandler(g *discordgo.GuildDelete) { func (user *User) guildDeleteHandler(g *discordgo.GuildDelete) {
if g.Unavailable {
user.log.Info().Str("guild_id", g.ID).Msg("Ignoring guild delete event with unavailable flag")
return
}
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)
@@ -1038,6 +1050,30 @@ func (user *User) guildUpdateHandler(g *discordgo.GuildUpdate) {
user.handleGuild(g.Guild, time.Now(), user.IsInSpace(g.ID)) user.handleGuild(g.Guild, time.Now(), user.IsInSpace(g.ID))
} }
func (user *User) threadListSyncHandler(t *discordgo.ThreadListSync) {
for _, meta := range t.Threads {
log := user.log.With().
Str("action", "thread list sync").
Str("guild_id", t.GuildID).
Str("parent_id", meta.ParentID).
Str("thread_id", meta.ID).
Logger()
ctx := log.WithContext(context.Background())
thread := user.bridge.GetThreadByID(meta.ID, nil)
if thread == nil {
msg := user.bridge.DB.Message.GetByDiscordID(database.NewPortalKey(meta.ParentID, ""), meta.ID)
if len(msg) == 0 {
log.Debug().Msg("Found unknown thread in thread list sync and don't have message")
} else {
log.Debug().Msg("Found unknown thread in thread list sync for existing message, creating thread")
user.bridge.threadFound(ctx, user, msg[0], meta.ID, meta)
}
} else {
thread.Parent.ForwardBackfillMissed(user, meta.LastMessageID, thread)
}
}
}
func (user *User) channelCreateHandler(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().
@@ -1169,11 +1205,21 @@ func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildI
return return
} }
portal.discordMessages <- portalDiscordMessage{ wrappedMsg := portalDiscordMessage{
msg: msg, msg: msg,
user: user, user: user,
thread: thread, thread: thread,
} }
select {
case portal.discordMessages <- wrappedMsg:
default:
user.log.Warn().
Str("discord_event", typeName).
Str("guild_id", guildID).
Str("channel_id", channelID).
Msg("Portal message buffer is full")
portal.discordMessages <- wrappedMsg
}
} }
type CustomReadReceipt struct { type CustomReadReceipt struct {
@@ -1409,6 +1455,8 @@ func (user *User) bridgeGuild(guildID string, everything bool) error {
} }
if everything { if everything {
guild.BridgingMode = database.GuildBridgeEverything guild.BridgingMode = database.GuildBridgeEverything
} else {
guild.BridgingMode = database.GuildBridgeCreateOnMessage
} }
guild.Update() guild.Update()