19 Commits

Author SHA1 Message Date
Tulir Asokan
19e26674e6 Bump version to v0.7.6 2026-02-16 15:49:46 +02:00
Tulir Asokan
daf6b9420c Add support for federation thumbnail endpoint 2026-02-15 21:48:10 +02:00
Tulir Asokan
fab784bfd8 Add new fields to uploads 2026-02-15 14:51:14 +02:00
Tulir Asokan
17c1938b4c Bump minimum Go version to 1.25 2026-02-15 14:48:14 +02:00
Tulir Asokan
ca9f032234 docker: fix working directory and update to Alpine 3.23 2026-02-12 16:29:35 +02:00
Tulir Asokan
5c4527f1b2 Disable restricted rooms by default 2026-01-21 19:09:42 +02:00
Skip R.
11b1ea5aa6 bump discordgo (#206) 2025-11-25 21:16:51 +02:00
Skip R.
d7292a0706 bump discordgo and add support for heartbeat sessions (#203) 2025-11-19 14:33:26 -08:00
Skip R.
9eaf213091 user: send errUserNotLoggedIn if we can't bridge event from logged-out user (#204) 2025-11-19 11:10:08 -08:00
Skip R.
c8c00a42bb Bump discordgo (#200) 2025-10-30 08:06:21 -07:00
Skip R.
2182c0d38f Only send CONNECTED bridge state on READY or RESUMED (#199) 2025-10-28 08:39:43 -07:00
Tulir Asokan
d92d7c4314 Install lottieconverter from Alpine repos 2025-08-19 17:44:20 +03:00
Tulir Asokan
5c22ed85a7 Bump minimum Go version to 1.24 2025-08-17 00:02:45 +03:00
Tulir Asokan
98e5e9de4a Revert "Allow v12 rooms to be created"
This reverts commit d2988096e4.

The bridge can handle v12 rooms fine, but creation requires additional considerations

Fixes #193
2025-08-17 00:02:28 +03:00
Tulir Asokan
820951cb6e Add support for disabling link previews via MSC4095 2025-08-10 23:47:39 +03:00
Tulir Asokan
52ebc21d9b Update mautrix-go 2025-08-10 23:28:01 +03:00
Tulir Asokan
16469259f7 Update issue templates 2025-07-18 17:30:19 +03:00
Tulir Asokan
d2988096e4 Allow v12 rooms to be created 2025-07-18 17:30:19 +03:00
Tulir Asokan
3f7622be19 Add support for following tombstones 2025-07-18 17:30:19 +03:00
18 changed files with 241 additions and 68 deletions

View File

@@ -1,14 +1,16 @@
---
name: Bug report
about: If something is definitely wrong in the bridge (rather than just a setup issue),
file a bug report. Remember to include relevant logs.
labels: bug
file a bug report. Remember to include relevant logs. Asking in the Matrix room first
is strongly recommended.
type: Bug
---
<!--
Remember to include relevant logs, the bridge version and any other details.
If you aren't sure what's needed, ask in the Matrix room rather than opening an
incomplete issue. Issues with insufficient detail will likely just be ignored.
It's always best to ask in the Matrix room first, especially if you aren't sure
what details are needed. Issues with insufficient detail will likely just be
ignored or closed immediately.
-->

View File

@@ -1,6 +1,6 @@
---
name: Enhancement request
about: Submit a feature request or other suggestion
labels: enhancement
type: Feature
---

View File

@@ -8,8 +8,8 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.23", "1.24"]
name: Lint ${{ matrix.go-version == '1.24' && '(latest)' || '(old)' }}
go-version: ["1.25", "1.26"]
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v4

View File

@@ -1,3 +1,15 @@
# v0.7.6 (2026-02-16)
* Bumped minimum Go version to 1.25.
* Updated Docker image to Alpine 3.23.
* Added support for following tombstones.
* Added support for disabling link previews in messages sent to Discord using
[MSC4095].
* Added support for federation thumbnail endpoint when using direct media.
* Disabled using `restricted` join rules by default.
[MSC4095]: https://github.com/matrix-org/matrix-spec-proposals/pull/4095
# v0.7.5 (2025-07-16)
* Fixed federation key response when using direct media.

View File

@@ -1,6 +1,4 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.22 AS lottie
FROM golang:1-alpine3.22 AS builder
FROM golang:1-alpine3.23 AS builder
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
@@ -8,19 +6,17 @@ COPY . /build
WORKDIR /build
RUN go build -o /usr/bin/mautrix-discord
FROM alpine:3.22
FROM alpine:3.23
ENV UID=1337 \
GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl \
zlib libpng giflib libstdc++ libgcc
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq curl yq-go lottieconverter
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
COPY --from=builder /usr/bin/mautrix-discord /usr/bin/mautrix-discord
COPY --from=builder /build/example-config.yaml /opt/mautrix-discord/example-config.yaml
COPY --from=builder /build/docker-run.sh /docker-run.sh
VOLUME /data
WORKDIR /data
CMD ["/docker-run.sh"]

View File

@@ -1,19 +1,15 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.22 AS lottie
FROM alpine:3.22
FROM alpine:3.23
ENV UID=1337 \
GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq \
zlib libpng giflib libstdc++ libgcc
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go lottieconverter
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
ARG EXECUTABLE=./mautrix-discord
COPY $EXECUTABLE /usr/bin/mautrix-discord
COPY ./example-config.yaml /opt/mautrix-discord/example-config.yaml
COPY ./docker-run.sh /docker-run.sh
VOLUME /data
WORKDIR /data
CMD ["/docker-run.sh"]

20
database/json.go Normal file
View File

@@ -0,0 +1,20 @@
package database
import (
"go.mau.fi/util/dbutil"
)
// Backported from mautrix/go-util@e5cb5e96d15cb87ffe6e5970c2f90ee47980e715.
// JSONPtr is a convenience function for wrapping a pointer to a value in the JSON utility, but removing typed nils
// (i.e. preventing nils from turning into the string "null" in the database).
func JSONPtr[T any](val *T) dbutil.JSON {
return dbutil.JSON{Data: UntypedNil(val)}
}
func UntypedNil[T any](val *T) any {
if val == nil {
return nil
}
return val
}

View File

@@ -1,4 +1,4 @@
-- v0 -> v23 (compatible with v19+): Latest revision
-- v0 -> v24 (compatible with v19+): Latest revision
CREATE TABLE guild (
dcid TEXT PRIMARY KEY,
@@ -92,7 +92,8 @@ CREATE TABLE "user" (
space_room TEXT,
dm_space_room TEXT,
read_state_version INTEGER NOT NULL DEFAULT 0
read_state_version INTEGER NOT NULL DEFAULT 0,
heartbeat_session jsonb
);
CREATE TABLE user_portal (

View File

@@ -0,0 +1,2 @@
-- v24 (compatible with v19+): Add persisted heartbeat sessions
ALTER TABLE "user" ADD COLUMN heartbeat_session jsonb;

View File

@@ -3,6 +3,7 @@ package database
import (
"database/sql"
"github.com/bwmarrin/discordgo"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
@@ -21,18 +22,18 @@ func (uq *UserQuery) New() *User {
}
func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
query := `SELECT mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version FROM "user" WHERE mxid=$1`
query := `SELECT mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version, heartbeat_session FROM "user" WHERE mxid=$1`
return uq.New().Scan(uq.db.QueryRow(query, userID))
}
func (uq *UserQuery) GetByID(id string) *User {
query := `SELECT mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version FROM "user" WHERE dcid=$1`
query := `SELECT mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version, heartbeat_session FROM "user" WHERE dcid=$1`
return uq.New().Scan(uq.db.QueryRow(query, id))
}
func (uq *UserQuery) GetAllWithToken() []*User {
query := `
SELECT mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version
SELECT mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version, heartbeat_session
FROM "user" WHERE discord_token IS NOT NULL
`
rows, err := uq.db.Query(query)
@@ -54,19 +55,20 @@ type User struct {
db *Database
log log.Logger
MXID id.UserID
DiscordID string
DiscordToken string
ManagementRoom id.RoomID
SpaceRoom id.RoomID
DMSpaceRoom id.RoomID
MXID id.UserID
DiscordID string
DiscordToken string
ManagementRoom id.RoomID
SpaceRoom id.RoomID
DMSpaceRoom id.RoomID
HeartbeatSession *discordgo.HeartbeatSession
ReadStateVersion int
}
func (u *User) Scan(row dbutil.Scannable) *User {
var discordID, managementRoom, spaceRoom, dmSpaceRoom, discordToken sql.NullString
err := row.Scan(&u.MXID, &discordID, &discordToken, &managementRoom, &spaceRoom, &dmSpaceRoom, &u.ReadStateVersion)
err := row.Scan(&u.MXID, &discordID, &discordToken, &managementRoom, &spaceRoom, &dmSpaceRoom, &u.ReadStateVersion, dbutil.JSON{Data: &u.HeartbeatSession})
if err != nil {
if err != sql.ErrNoRows {
u.log.Errorln("Database scan failed:", err)
@@ -83,8 +85,8 @@ func (u *User) Scan(row dbutil.Scannable) *User {
}
func (u *User) Insert() {
query := `INSERT INTO "user" (mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version) VALUES ($1, $2, $3, $4, $5, $6, $7)`
_, err := u.db.Exec(query, u.MXID, strPtr(u.DiscordID), strPtr(u.DiscordToken), strPtr(string(u.ManagementRoom)), strPtr(string(u.SpaceRoom)), strPtr(string(u.DMSpaceRoom)), u.ReadStateVersion)
query := `INSERT INTO "user" (mxid, dcid, discord_token, management_room, space_room, dm_space_room, read_state_version, heartbeat_session) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
_, err := u.db.Exec(query, u.MXID, strPtr(u.DiscordID), strPtr(u.DiscordToken), strPtr(string(u.ManagementRoom)), strPtr(string(u.SpaceRoom)), strPtr(string(u.DMSpaceRoom)), u.ReadStateVersion, JSONPtr(u.HeartbeatSession))
if err != nil {
u.log.Warnfln("Failed to insert %s: %v", u.MXID, err)
panic(err)
@@ -92,8 +94,8 @@ func (u *User) Insert() {
}
func (u *User) Update() {
query := `UPDATE "user" SET dcid=$1, discord_token=$2, management_room=$3, space_room=$4, dm_space_room=$5, read_state_version=$6 WHERE mxid=$7`
_, err := u.db.Exec(query, strPtr(u.DiscordID), strPtr(u.DiscordToken), strPtr(string(u.ManagementRoom)), strPtr(string(u.SpaceRoom)), strPtr(string(u.DMSpaceRoom)), u.ReadStateVersion, u.MXID)
query := `UPDATE "user" SET dcid=$1, discord_token=$2, management_room=$3, space_room=$4, dm_space_room=$5, read_state_version=$6, heartbeat_session=$7 WHERE mxid=$8`
_, err := u.db.Exec(query, strPtr(u.DiscordID), strPtr(u.DiscordToken), strPtr(string(u.ManagementRoom)), strPtr(string(u.SpaceRoom)), strPtr(string(u.DMSpaceRoom)), u.ReadStateVersion, JSONPtr(u.HeartbeatSession), u.MXID)
if err != nil {
u.log.Warnfln("Failed to update %q: %v", u.MXID, err)
panic(err)

View File

@@ -154,6 +154,7 @@ func newDirectMediaAPI(br *DiscordBridge) *DirectMediaAPI {
addRoutes("r0")
addRoutes("v1")
federationRouter.HandleFunc("/v1/media/download/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet)
federationRouter.HandleFunc("/v1/media/thumbnail/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet)
federationRouter.HandleFunc("/v1/version", dma.ks.GetServerVersion).Methods(http.MethodGet)
mediaRouter.NotFoundHandler = http.HandlerFunc(dma.UnknownEndpoint)
mediaRouter.MethodNotAllowedHandler = http.HandlerFunc(dma.UnsupportedMethod)
@@ -556,7 +557,7 @@ func (dma *DirectMediaAPI) proxyDownload(ctx context.Context, w http.ResponseWri
func (dma *DirectMediaAPI) DownloadMedia(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := zerolog.Ctx(ctx)
isNewFederation := strings.HasPrefix(r.URL.Path, "/_matrix/federation/v1/media/download/")
isNewFederation := strings.HasPrefix(r.URL.Path, "/_matrix/federation/v1/media/")
vars := mux.Vars(r)
if !isNewFederation && vars["serverName"] != dma.cfg.ServerName {
jsonResponse(w, http.StatusNotFound, &mautrix.RespError{

View File

@@ -130,7 +130,7 @@ bridge:
message_error_notices: true
# Should the bridge use space-restricted join rules instead of invite-only for guild rooms?
# This can avoid unnecessary invite events in guild rooms when members are synced in.
restricted_rooms: true
restricted_rooms: false
# Should the bridge automatically join the user to threads on Discord when the thread is opened on Matrix?
# This only works with clients that support thread read receipts (MSC3771 added in Matrix v1.4).
autojoin_thread_on_open: true

View File

@@ -98,6 +98,7 @@ func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLink
const formatterContextPortalKey = "fi.mau.discord.portal"
const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions"
const formatterContextInputAllowedMentionsKey = "fi.mau.discord.input_allowed_mentions"
const formatterContextInputAllowedLinkPreviewsKey = "fi.mau.discord.input_allowed_link_previews"
func appendIfNotContains(arr []string, newItem string) []string {
for _, item := range arr {
@@ -221,16 +222,24 @@ var matrixHTMLParser = &format.HTMLParser{
return fmt.Sprintf("||%s||", text)
},
LinkConverter: func(text, href string, ctx format.Context) string {
linkPreviews := ctx.ReturnData[formatterContextInputAllowedLinkPreviewsKey].([]string)
allowPreview := linkPreviews == nil || slices.Contains(linkPreviews, href)
if text == href {
if !allowPreview {
return fmt.Sprintf("<%s>", text)
}
return text
} else if !discordLinkRegexFull.MatchString(href) {
return fmt.Sprintf("%s (%s)", escapeDiscordMarkdown(text), escapeDiscordMarkdown(href))
} else if !allowPreview {
return fmt.Sprintf("[%s](<%s>)", escapeDiscordMarkdown(text), href)
} else {
return fmt.Sprintf("[%s](%s)", escapeDiscordMarkdown(text), href)
}
return fmt.Sprintf("[%s](%s)", escapeDiscordMarkdown(text), href)
},
}
func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) (string, *discordgo.MessageAllowedMentions) {
func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent, allowedLinkPreviews []string) (string, *discordgo.MessageAllowedMentions) {
allowedMentions := &discordgo.MessageAllowedMentions{
Parse: []discordgo.AllowedMentionType{},
Users: []string{},
@@ -238,6 +247,7 @@ func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) (strin
}
if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 {
ctx := format.NewContext()
ctx.ReturnData[formatterContextInputAllowedLinkPreviewsKey] = allowedLinkPreviews
ctx.ReturnData[formatterContextPortalKey] = portal
ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions
if content.Mentions != nil {

9
go.mod
View File

@@ -1,8 +1,8 @@
module go.mau.fi/mautrix-discord
go 1.23.0
go 1.25.0
toolchain go1.24.5
toolchain go1.26.0
require (
github.com/bwmarrin/discordgo v0.27.0
@@ -20,12 +20,13 @@ require (
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc
golang.org/x/sync v0.16.0
maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.16.3-0.20250629161415-a29d782e6638
maunium.net/go/mautrix v0.16.3-0.20250810202616-6bc5698125c2
)
require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -42,4 +43,4 @@ require (
maunium.net/go/mauflag v1.0.0 // indirect
)
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20250607214857-f23a8518ece2
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20260215125047-ccf8cbaa0a9f

10
go.sum
View File

@@ -1,7 +1,7 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/beeper/discordgo v0.0.0-20250607214857-f23a8518ece2 h1:8lgTjYGSIlS90f0jiFfEC4UwxCq9FiUo4dKwjknbupQ=
github.com/beeper/discordgo v0.0.0-20250607214857-f23a8518ece2/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA=
github.com/beeper/discordgo v0.0.0-20260215125047-ccf8cbaa0a9f h1:A+SRmETpSnFixbP1x6u7sQdoi8cOuYfL5bkDJy9F/Pg=
github.com/beeper/discordgo v0.0.0-20260215125047-ccf8cbaa0a9f/go.mod h1:lioivnibvB8j1KcF5TVpLdRLKCKHtcl8A03GpxRCre4=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -11,6 +11,8 @@ github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFA
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -73,5 +75,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.16.3-0.20250629161415-a29d782e6638 h1:tZEmq1FBuueW6cmxoa2BkXYbfMknW8XPPMiZfD5PKto=
maunium.net/go/mautrix v0.16.3-0.20250629161415-a29d782e6638/go.mod h1:gCgLw/4c1a8QsiOWTdUdXlt5cYdE0rJ9wLeZQKPD58Q=
maunium.net/go/mautrix v0.16.3-0.20250810202616-6bc5698125c2 h1:8PdwIklPNHTL/tI9tG2S0Tf9UvAgRt8yZjJbjV0XIpA=
maunium.net/go/mautrix v0.16.3-0.20250810202616-6bc5698125c2/go.mod h1:gCgLw/4c1a8QsiOWTdUdXlt5cYdE0rJ9wLeZQKPD58Q=

View File

@@ -26,6 +26,7 @@ import (
"golang.org/x/sync/semaphore"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/config"
@@ -95,6 +96,7 @@ func (br *DiscordBridge) GetConfigPtr() interface{} {
func (br *DiscordBridge) Init() {
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
br.RegisterCommands()
br.EventProcessor.On(event.StateTombstone, br.HandleTombstone)
matrixHTMLParser.PillConverter = br.pillConverter
@@ -185,7 +187,7 @@ func main() {
Name: "mautrix-discord",
URL: "https://github.com/mautrix/discord",
Description: "A Matrix-Discord puppeting bridge.",
Version: "0.7.5",
Version: "0.7.6",
ProtocolName: "Discord",
BeeperServiceName: "discordgo",
BeeperNetworkName: "discord",

142
portal.go
View File

@@ -1522,9 +1522,15 @@ func (portal *Portal) RefererOptIfUser(sess *discordgo.Session, threadID string)
}
func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return
if portal.IsPrivateChat() {
if !sender.IsLoggedIn() {
go portal.sendMessageMetrics(evt, errUserNotLoggedIn, "Ignoring")
return
}
if sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return
}
}
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
@@ -1545,7 +1551,8 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
if editMXID := content.GetRelatesTo().GetReplaceID(); editMXID != "" && content.NewContent != nil {
edits := portal.bridge.DB.Message.GetByMXID(portal.Key, editMXID)
if edits != nil {
discordContent, allowedMentions := portal.parseMatrixHTML(content.NewContent)
newContentRaw, _ := evt.Content.Raw["m.new_content"].(map[string]any)
discordContent, allowedMentions := portal.parseMatrixHTML(content.NewContent, parseAllowedLinkPreviews(newContentRaw))
var err error
var msg *discordgo.Message
if !isWebhookSend {
@@ -1624,7 +1631,7 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
}
switch content.MsgType {
case event.MsgText, event.MsgEmote, event.MsgNotice:
sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content)
sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content, parseAllowedLinkPreviews(evt.Content.Raw))
if content.MsgType == event.MsgEmote {
sendReq.Content = fmt.Sprintf("_%s_", sendReq.Content)
}
@@ -1637,7 +1644,7 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
filename := content.Body
if content.FileName != "" && content.FileName != content.Body {
filename = content.FileName
sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content)
sendReq.Content, sendReq.AllowedMentions = portal.parseMatrixHTML(content, parseAllowedLinkPreviews(evt.Content.Raw))
}
if evt.Content.Raw["page.codeberg.everypizza.msc4193.spoiler"] == true {
@@ -1646,16 +1653,21 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
if portal.bridge.Config.Bridge.UseDiscordCDNUpload && !isWebhookSend && sess.IsUser {
att := &discordgo.MessageAttachment{
ID: "0",
Filename: filename,
Description: description,
ID: "0",
Filename: filename,
Description: description,
OriginalContentType: content.Info.MimeType,
}
sendReq.Attachments = []*discordgo.MessageAttachment{att}
isClip := false
prep, err := sender.Session.ChannelAttachmentCreate(channelID, &discordgo.ReqPrepareAttachments{
Files: []*discordgo.FilePrepare{{
Size: len(data),
Name: att.Filename,
ID: sender.NextDiscordUploadID(),
IsClip: &isClip,
OriginalContentType: att.OriginalContentType,
}},
}, portal.RefererOpt(threadID))
if err != nil {
@@ -1745,6 +1757,28 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
}
}
func parseAllowedLinkPreviews(raw map[string]any) []string {
if raw == nil {
return nil
}
linkPreviews, ok := raw["com.beeper.linkpreviews"].([]any)
if !ok {
return nil
}
allowedLinkPreviews := make([]string, 0, len(linkPreviews))
for _, preview := range linkPreviews {
previewMap, ok := preview.(map[string]any)
if !ok {
continue
}
matchedURL, _ := previewMap["matched_url"].(string)
if matchedURL != "" {
allowedLinkPreviews = append(allowedLinkPreviews, matchedURL)
}
}
return allowedLinkPreviews
}
func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) {
if portal.bridge.Config.Bridge.DeliveryReceipts {
err := portal.bridge.Bot.MarkRead(portal.MXID, eventID)
@@ -1895,12 +1929,13 @@ func (portal *Portal) getMatrixUsers() ([]id.UserID, error) {
}
func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) {
if !sender.IsLoggedIn() {
go portal.sendMessageMetrics(evt, errUserNotLoggedIn, "Ignoring")
return
}
if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return
} else if !sender.IsLoggedIn() {
//go portal.sendMessageMetrics(evt, errReactionUserNotLoggedIn, "Ignoring")
return
}
reaction := evt.Content.AsReaction()
@@ -2078,9 +2113,15 @@ func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.Mess
}
func (portal *Portal) handleMatrixRedaction(sender *User, evt *event.Event) {
if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return
if portal.IsPrivateChat() {
if !sender.IsLoggedIn() {
go portal.sendMessageMetrics(evt, errUserNotLoggedIn, "Ignoring")
return
}
if sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return
}
}
sess := sender.Session
@@ -2535,3 +2576,74 @@ func (portal *Portal) UpdateInfo(source *User, meta *discordgo.Channel) *discord
}
return meta
}
func (br *DiscordBridge) HandleTombstone(evt *event.Event) {
if evt.StateKey == nil || *evt.StateKey != "" {
return
}
content, ok := evt.Content.Parsed.(*event.TombstoneEventContent)
if !ok {
return
}
defer br.MatrixHandler.TrackEventDuration(evt.Type)()
portal := br.GetPortalByMXID(evt.RoomID)
if portal == nil {
return
}
logEvt := portal.log.Debug().
Stringer("sender", evt.Sender).
Stringer("replacement_room", content.ReplacementRoom).
Str("body", content.Body)
if content.ReplacementRoom == "" {
logEvt.Msg("Received tombstone event with no replacement room, cleaning up portal")
portal.cleanup(true)
portal.RemoveMXID()
return
}
logEvt.Msg("Received tombstone event, joining new room")
_, err := br.Bot.JoinRoom(content.ReplacementRoom.String(), evt.Sender.Homeserver(), nil)
if err != nil {
portal.log.Err(err).Msg("Failed to join replacement room")
return
}
_, err = br.Bot.State(content.ReplacementRoom)
if err != nil {
portal.log.Err(err).Msg("Failed to get state of replacement room")
return
}
encrypted := br.AS.StateStore.IsEncrypted(portal.MXID)
br.portalsLock.Lock()
defer br.portalsLock.Unlock()
if portal.MXID != evt.RoomID {
portal.log.Warn().
Stringer("old_mxid", evt.RoomID).
Stringer("new_mxid", portal.MXID).
Msg("Portal MXID changed while processing tombstone event, not updating")
return
}
_, alreadyAPortal := br.portalsByMXID[content.ReplacementRoom]
if alreadyAPortal {
portal.log.Warn().
Stringer("replacement_room", content.ReplacementRoom).
Msg("Replacement room is already a portal, not updating")
return
}
delete(portal.bridge.portalsByMXID, portal.MXID)
portal.MXID = content.ReplacementRoom
portal.bridge.portalsByMXID[portal.MXID] = portal
portal.log = portal.bridge.ZLog.With().
Str("channel_id", portal.Key.ChannelID).
Str("channel_receiver", portal.Key.Receiver).
Str("room_id", portal.MXID.String()).
Logger()
portal.AvatarSet = false
portal.NameSet = false
portal.TopicSet = false
portal.Encrypted = encrypted
portal.InSpace = ""
portal.FirstEventID = ""
portal.Update()
portal.log.Info().Msg("Followed tombstone and updated portal MXID")
portal.UpdateBridgeInfo()
}

18
user.go
View File

@@ -550,6 +550,17 @@ func (user *User) Connect() error {
if err != nil {
return err
}
if user.HeartbeatSession == nil || user.HeartbeatSession.IsExpired() {
user.log.Debug().Msg("Creating new heartbeat session")
sess := discordgo.NewHeartbeatSession()
user.HeartbeatSession = &sess
}
user.HeartbeatSession.BumpLastUsed()
user.Update()
// make discordgo use our session instead of the one it creates automatically
session.HeartbeatSession = *user.HeartbeatSession
if user.bridge.Config.Bridge.Proxy != "" {
u, _ := url.Parse(user.bridge.Config.Bridge.Proxy)
tlsConf := &tls.Config{
@@ -569,7 +580,10 @@ func (user *User) Connect() error {
} else {
session.LogLevel = discordgo.LogInformational
}
userDiscordLog := user.log.With().Str("component", "discordgo").Logger()
userDiscordLog := user.log.With().
Str("component", "discordgo").
Str("heartbeat_session", session.HeartbeatSession.ID.String()).
Logger()
session.Logger = func(msgL, caller int, format string, a ...interface{}) {
userDiscordLog.WithLevel(discordToZeroLevel(msgL)).Caller(caller+1).Msgf(strings.TrimSpace(format), a...) // zerolog-allow-msgf
}
@@ -807,6 +821,7 @@ func (user *User) subscribeGuilds(delay time.Duration) {
func (user *User) resumeHandler(_ *discordgo.Resumed) {
user.log.Debug().Msg("Discord connection resumed")
user.subscribeGuilds(0 * time.Second)
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
}
func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
@@ -1007,7 +1022,6 @@ func (user *User) connectedHandler(_ *discordgo.Connect) {
user.log.Debug().Msg("Connected to Discord")
if user.wasDisconnected {
user.wasDisconnected = false
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
}
}