41 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
Tulir Asokan
40a6992151 Bump version to v0.7.5 2025-07-16 11:45:58 +03:00
Tulir Asokan
111824486b Hardcode v11 for new rooms
Upcoming breaking changes in room v12 prevent safely using the default
room version and security embargoes prevent fixing them ahead of time.
2025-07-15 14:43:53 +03:00
Tulir Asokan
d4e7289315 Update prefix_webhook_messages option to use MSC4144 fallbacks 2025-07-01 00:59:17 +03:00
Tulir Asokan
e2151defc6 Update mautrix-go to fix federation key response
[skip cd]
2025-06-29 19:15:05 +03:00
Tulir Asokan
30c2cd94a7 Bump version to v0.7.4 2025-06-16 18:53:05 +03:00
Tulir Asokan
847d4cb98e Update Docker image to Alpine 3.22
Closes #183
2025-06-08 00:58:00 +03:00
Tulir Asokan
9fd89cdfc5 Add support for forwarded messages
Fixes #170
Closes #182
2025-06-08 00:49:16 +03:00
LeaPhant
dc4538aab6 Add support for MSC4193 media spoilers (#189) 2025-06-08 00:27:29 +03:00
Tulir Asokan
a6fca7ce43 Add channel is bridgeable check to channel update handler 2025-06-08 00:24:19 +03:00
Tulir Asokan
d69e4e9881 Update mautrix-go to rename cross-room reply field 2025-06-08 00:15:45 +03:00
Tulir Asokan
ccc6c77911 Update mautrix-go to enable MSC4190. Closes #181 2025-05-03 22:13:06 +03:00
Tulir Asokan
001c88c400 Bump version to v0.7.3 2025-04-16 14:09:51 +03:00
Ping Chen
d37b5028e1 portal: fix isPlainGifMessage to get link preview working (#179) 2025-04-09 20:37:25 +09:00
Tulir Asokan
ef093b129f dependencies: update discordgo 2025-03-20 17:43:03 +02:00
batuhan
84e56c73fa portal: add com.beeper.room_type.v2 to m.bridge events (#178) 2025-03-20 16:23:00 +02:00
ginnyTheCat
5854ad0c14 portal: fix typo in msc1767 field name (#177) 2025-03-20 16:22:37 +02:00
Tulir Asokan
9605992758 portal: add id field for per-message profiles
Closes #176
2025-03-20 16:21:38 +02:00
Tulir Asokan
4d67dbcd00 client: only load main page for users 2025-02-22 20:09:40 +02:00
Tulir Asokan
31a75d871f login: add filename when sending QR image 2025-02-22 20:09:36 +02:00
Tulir Asokan
b8892ed59f dependencies: update 2025-02-22 20:04:47 +02:00
Tulir Asokan
65ef2c4ff6 portal: add support for no-mention replies 2025-02-22 19:48:16 +02:00
mat
4a8e9f5c21 portal: fix per-message profiles for guild-specific avatars (#172) 2025-02-09 02:34:44 +02:00
23 changed files with 437 additions and 152 deletions

View File

@@ -1,14 +1,16 @@
--- ---
name: Bug report name: Bug report
about: If something is definitely wrong in the bridge (rather than just a setup issue), about: If something is definitely wrong in the bridge (rather than just a setup issue),
file a bug report. Remember to include relevant logs. file a bug report. Remember to include relevant logs. Asking in the Matrix room first
labels: bug is strongly recommended.
type: Bug
--- ---
<!-- <!--
Remember to include relevant logs, the bridge version and any other details. 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 It's always best to ask in the Matrix room first, especially if you aren't sure
incomplete issue. Issues with insufficient detail will likely just be ignored. 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 name: Enhancement request
about: Submit a feature request or other suggestion about: Submit a feature request or other suggestion
labels: enhancement type: Feature
--- ---

View File

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

View File

@@ -1,3 +1,51 @@
# 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.
* Changed `prefix_webhook_messages` option to generate [MSC4144] fallbacks,
so that any compatible clients will hide the prefix.
* Changed new room creation to hardcode room v11 to avoid v12 rooms being
created before proper support for them can be added.
# v0.7.4 (2025-06-16)
* Added support for forwarded messages
* Added support for [MSC4193] media spoilers (thanks to [@LeaPhant] in [#189]).
* Added support for [MSC4190] for MAS-compatible encryption.
* Updated Docker image to Alpine 3.22
[MSC4193]: https://github.com/matrix-org/matrix-spec-proposals/pull/4193
[MSC4190]: https://github.com/matrix-org/matrix-spec-proposals/pull/4190
[@LeaPhant]: https://github.com/mautrix/discord/pull/189
[#189]: https://github.com/mautrix/discord/pull/189
# v0.7.3 (2025-04-16)
* Added support for sending no-mention replies from Matrix
(uses intentional mentions and requires client support).
* Added file name to QR image message when logging in to fix rendering in dumb
clients that validate the file extension.
* Added `id` field to per-message profiles to match [MSC4144].
* Fixed guild avatars in per-message profiles (thanks to [@mat-1] in [#172]).
* Fixed typo in MSC1767 field name in voice messages (thanks to [@ginnyTheCat] in [#177]).
[@mat-1]: https://github.com/mat-1
[@ginnyTheCat]: https://github.com/ginnyTheCat
[#172]: https://github.com/mautrix/discord/pull/172
[#177]: https://github.com/mautrix/discord/pull/177
[MSC4144]: https://github.com/matrix-org/matrix-spec-proposals/pull/4144
# v0.7.2 (2024-12-16) # v0.7.2 (2024-12-16)
* Fixed some headers being set incorrectly. * Fixed some headers being set incorrectly.

View File

@@ -1,6 +1,4 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18 AS lottie FROM golang:1-alpine3.23 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,19 +6,17 @@ 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.18 FROM alpine:3.23
ENV UID=1337 \ ENV UID=1337 \
GID=1337 GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl \ RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq curl yq-go lottieconverter
zlib libpng giflib libstdc++ libgcc
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 /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/example-config.yaml /opt/mautrix-discord/example-config.yaml
COPY --from=builder /build/docker-run.sh /docker-run.sh COPY --from=builder /build/docker-run.sh /docker-run.sh
VOLUME /data VOLUME /data
WORKDIR /data
CMD ["/docker-run.sh"] CMD ["/docker-run.sh"]

View File

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

View File

@@ -1,18 +0,0 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18 AS lottie
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 \
zlib libpng giflib libstdc++ libgcc
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
COPY . /build
WORKDIR /build
RUN go build -o /usr/bin/mautrix-discord
# Setup development stack using gow
RUN go install github.com/mitranim/gow@latest
RUN echo 'gow run /build $@' > /usr/bin/mautrix-discord \
&& chmod +x /usr/bin/mautrix-discord
VOLUME /data

View File

@@ -239,9 +239,10 @@ func sendQRCode(ce *WrappedCommandEvent, code string) id.EventID {
} }
content := event.MessageEventContent{ content := event.MessageEventContent{
MsgType: event.MsgImage, MsgType: event.MsgImage,
Body: code, Body: code,
URL: url.CUString(), FileName: "qr.png",
URL: url.CUString(),
} }
resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content) resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content)

View File

@@ -99,6 +99,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "encryption", "default") helper.Copy(up.Bool, "bridge", "encryption", "default")
helper.Copy(up.Bool, "bridge", "encryption", "require") helper.Copy(up.Bool, "bridge", "encryption", "require")
helper.Copy(up.Bool, "bridge", "encryption", "appservice") helper.Copy(up.Bool, "bridge", "encryption", "appservice")
helper.Copy(up.Bool, "bridge", "encryption", "msc4190")
helper.Copy(up.Bool, "bridge", "encryption", "allow_key_sharing") helper.Copy(up.Bool, "bridge", "encryption", "allow_key_sharing")
helper.Copy(up.Bool, "bridge", "encryption", "plaintext_mentions") helper.Copy(up.Bool, "bridge", "encryption", "plaintext_mentions")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outbound_on_ack") helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outbound_on_ack")

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 ( CREATE TABLE guild (
dcid TEXT PRIMARY KEY, dcid TEXT PRIMARY KEY,
@@ -92,7 +92,8 @@ CREATE TABLE "user" (
space_room TEXT, space_room TEXT,
dm_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 ( 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 ( import (
"database/sql" "database/sql"
"github.com/bwmarrin/discordgo"
"go.mau.fi/util/dbutil" "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"
@@ -21,18 +22,18 @@ func (uq *UserQuery) New() *User {
} }
func (uq *UserQuery) GetByMXID(userID id.UserID) *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)) return uq.New().Scan(uq.db.QueryRow(query, userID))
} }
func (uq *UserQuery) GetByID(id string) *User { 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)) return uq.New().Scan(uq.db.QueryRow(query, id))
} }
func (uq *UserQuery) GetAllWithToken() []*User { func (uq *UserQuery) GetAllWithToken() []*User {
query := ` 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 FROM "user" WHERE discord_token IS NOT NULL
` `
rows, err := uq.db.Query(query) rows, err := uq.db.Query(query)
@@ -54,19 +55,20 @@ type User struct {
db *Database db *Database
log log.Logger log log.Logger
MXID id.UserID MXID id.UserID
DiscordID string DiscordID string
DiscordToken string DiscordToken string
ManagementRoom id.RoomID ManagementRoom id.RoomID
SpaceRoom id.RoomID SpaceRoom id.RoomID
DMSpaceRoom id.RoomID DMSpaceRoom id.RoomID
HeartbeatSession *discordgo.HeartbeatSession
ReadStateVersion int ReadStateVersion int
} }
func (u *User) Scan(row dbutil.Scannable) *User { func (u *User) Scan(row dbutil.Scannable) *User {
var discordID, managementRoom, spaceRoom, dmSpaceRoom, discordToken sql.NullString 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 != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
u.log.Errorln("Database scan failed:", err) u.log.Errorln("Database scan failed:", err)
@@ -83,8 +85,8 @@ func (u *User) Scan(row dbutil.Scannable) *User {
} }
func (u *User) Insert() { 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)` 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) _, 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 { if err != nil {
u.log.Warnfln("Failed to insert %s: %v", u.MXID, err) u.log.Warnfln("Failed to insert %s: %v", u.MXID, err)
panic(err) panic(err)
@@ -92,8 +94,8 @@ func (u *User) Insert() {
} }
func (u *User) Update() { 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` 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, u.MXID) _, 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 { if err != nil {
u.log.Warnfln("Failed to update %q: %v", u.MXID, err) u.log.Warnfln("Failed to update %q: %v", u.MXID, err)
panic(err) panic(err)

View File

@@ -154,6 +154,7 @@ func newDirectMediaAPI(br *DiscordBridge) *DirectMediaAPI {
addRoutes("r0") addRoutes("r0")
addRoutes("v1") addRoutes("v1")
federationRouter.HandleFunc("/v1/media/download/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet) 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) federationRouter.HandleFunc("/v1/version", dma.ks.GetServerVersion).Methods(http.MethodGet)
mediaRouter.NotFoundHandler = http.HandlerFunc(dma.UnknownEndpoint) mediaRouter.NotFoundHandler = http.HandlerFunc(dma.UnknownEndpoint)
mediaRouter.MethodNotAllowedHandler = http.HandlerFunc(dma.UnsupportedMethod) 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) { func (dma *DirectMediaAPI) DownloadMedia(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
log := zerolog.Ctx(ctx) 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) vars := mux.Vars(r)
if !isNewFederation && vars["serverName"] != dma.cfg.ServerName { if !isNewFederation && vars["serverName"] != dma.cfg.ServerName {
jsonResponse(w, http.StatusNotFound, &mautrix.RespError{ jsonResponse(w, http.StatusNotFound, &mautrix.RespError{

View File

@@ -91,7 +91,7 @@ bridge:
# .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 and is not an application # .Webhook - Whether the user is a webhook and is not an application
# .Application - Whether the user is an application # .Application - Whether the user is an application
displayname_template: '{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}' displayname_template: '{{if .Webhook}}Webhook{{else}}{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}{{end}}'
# Displayname template for Discord channels (bridged as rooms, or spaces when type=4). # Displayname template for Discord channels (bridged as rooms, or spaces when type=4).
# Available variables: # Available variables:
# .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs. # .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs.
@@ -130,7 +130,7 @@ bridge:
message_error_notices: true message_error_notices: true
# Should the bridge use space-restricted join rules instead of invite-only for guild rooms? # 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. # 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? # 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). # This only works with clients that support thread read receipts (MSC3771 added in Matrix v1.4).
autojoin_thread_on_open: true autojoin_thread_on_open: true
@@ -161,9 +161,12 @@ bridge:
federate_rooms: true federate_rooms: true
# Prefix messages from webhooks with the profile info? This can be used along with a custom displayname_template # Prefix messages from webhooks with the profile info? This can be used along with a custom displayname_template
# to better handle webhooks that change their name all the time (like ones used by bridges). # to better handle webhooks that change their name all the time (like ones used by bridges).
prefix_webhook_messages: false #
# This will use the fallback mode in MSC4144, which means clients that support MSC4144 will not show the prefix
# (and will instead show the name and avatar as the message sender).
prefix_webhook_messages: true
# Bridge webhook avatars? # Bridge webhook avatars?
enable_webhook_avatars: true enable_webhook_avatars: false
# Should the bridge upload media to the Discord CDN directly before sending the message when using a user token, # Should the bridge upload media to the Discord CDN directly before sending the message when using a user token,
# like the official client does? The other option is sending the media in the message send request as a form part # like the official client does? The other option is sending the media in the message send request as a form part
# (which is always used by bots and webhooks). # (which is always used by bots and webhooks).
@@ -266,7 +269,13 @@ bridge:
# This will cause the bridge bot to be in private chats for the encryption to work properly. # This will cause the bridge bot to be in private chats for the encryption to work properly.
default: false default: false
# Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data. # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.
# Changing this option requires updating the appservice registration file.
appservice: false appservice: false
# Whether to use MSC4190 instead of appservice login to create the bridge bot device.
# Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202.
# Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861).
# Changing this option requires updating the appservice registration file.
msc4190: false
# Require encryption, drop any unencrypted messages. # Require encryption, drop any unencrypted messages.
require: false require: false
# Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.

View File

@@ -74,7 +74,7 @@ var discordRendererWithInlineLinks = goldmark.New(
fixIndentedParagraphs, format.HTMLOptions, discordExtensions, fixIndentedParagraphs, format.HTMLOptions, discordExtensions,
) )
func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLinks bool) string { func (portal *Portal) renderDiscordMarkdownOnlyHTMLNoUnwrap(text string, allowInlineLinks bool) string {
text = escapeFixer.ReplaceAllStringFunc(text, escapeReplacement) text = escapeFixer.ReplaceAllStringFunc(text, escapeReplacement)
var buf strings.Builder var buf strings.Builder
@@ -88,12 +88,17 @@ func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLink
if err != nil { if err != nil {
panic(fmt.Errorf("markdown parser errored: %w", err)) panic(fmt.Errorf("markdown parser errored: %w", err))
} }
return format.UnwrapSingleParagraph(buf.String()) return buf.String()
}
func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLinks bool) string {
return format.UnwrapSingleParagraph(portal.renderDiscordMarkdownOnlyHTMLNoUnwrap(text, allowInlineLinks))
} }
const formatterContextPortalKey = "fi.mau.discord.portal" const formatterContextPortalKey = "fi.mau.discord.portal"
const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions" const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions"
const formatterContextInputAllowedMentionsKey = "fi.mau.discord.input_allowed_mentions" const formatterContextInputAllowedMentionsKey = "fi.mau.discord.input_allowed_mentions"
const formatterContextInputAllowedLinkPreviewsKey = "fi.mau.discord.input_allowed_link_previews"
func appendIfNotContains(arr []string, newItem string) []string { func appendIfNotContains(arr []string, newItem string) []string {
for _, item := range arr { for _, item := range arr {
@@ -217,16 +222,24 @@ var matrixHTMLParser = &format.HTMLParser{
return fmt.Sprintf("||%s||", text) return fmt.Sprintf("||%s||", text)
}, },
LinkConverter: func(text, href string, ctx format.Context) string { 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 text == href {
if !allowPreview {
return fmt.Sprintf("<%s>", text)
}
return text return text
} else if !discordLinkRegexFull.MatchString(href) { } else if !discordLinkRegexFull.MatchString(href) {
return fmt.Sprintf("%s (%s)", escapeDiscordMarkdown(text), escapeDiscordMarkdown(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{ allowedMentions := &discordgo.MessageAllowedMentions{
Parse: []discordgo.AllowedMentionType{}, Parse: []discordgo.AllowedMentionType{},
Users: []string{}, Users: []string{},
@@ -234,6 +247,7 @@ func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) (strin
} }
if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 { if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 {
ctx := format.NewContext() ctx := format.NewContext()
ctx.ReturnData[formatterContextInputAllowedLinkPreviewsKey] = allowedLinkPreviews
ctx.ReturnData[formatterContextPortalKey] = portal ctx.ReturnData[formatterContextPortalKey] = portal
ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions
if content.Mentions != nil { if content.Mentions != nil {

27
go.mod
View File

@@ -1,31 +1,32 @@
module go.mau.fi/mautrix-discord module go.mau.fi/mautrix-discord
go 1.22.0 go 1.25.0
toolchain go1.23.4 toolchain go1.26.0
require ( require (
github.com/bwmarrin/discordgo v0.27.0 github.com/bwmarrin/discordgo v0.27.0
github.com/gabriel-vasile/mimetype v1.4.7 github.com/gabriel-vasile/mimetype v1.4.9
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-sqlite3 v1.14.28
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.34.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.10.0 github.com/stretchr/testify v1.10.0
github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark v1.7.12
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc
golang.org/x/sync v0.10.0 golang.org/x/sync v0.16.0
maunium.net/go/maulogger/v2 v2.4.1 maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c maunium.net/go/mautrix v0.16.3-0.20250810202616-6bc5698125c2
) )
require ( require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // 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-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -34,12 +35,12 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // 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.31.0 // indirect golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.32.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.34.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-20241129182205-d6dffc9bf133 replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20260215125047-ccf8cbaa0a9f

48
go.sum
View File

@@ -1,16 +1,18 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 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/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/beeper/discordgo v0.0.0-20241129182205-d6dffc9bf133 h1:vTaMCjc0g0Cexadep0CaDxoJb0S37ZcUl5zTlDJ2adQ= github.com/beeper/discordgo v0.0.0-20260215125047-ccf8cbaa0a9f h1:A+SRmETpSnFixbP1x6u7sQdoi8cOuYfL5bkDJy9F/Pg=
github.com/beeper/discordgo v0.0.0-20241129182205-d6dffc9bf133/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA= 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 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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -22,14 +24,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -44,25 +46,25 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/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.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb h1:Is+6vDKgINRy9KHodvi7NElxoDaWA8sc2S3cF3+QWjs= go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb h1:Is+6vDKgINRy9KHodvi7NElxoDaWA8sc2S3cF3+QWjs=
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb/go.mod h1:tiBX6nxVSOjU89jVQ7wBh3P8KjM26Lv1k7/I5QdSvBw= go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb/go.mod h1:tiBX6nxVSOjU89jVQ7wBh3P8KjM26Lv1k7/I5QdSvBw=
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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@@ -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/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.16.3-0.20240712164054-e6046fbf432c h1:LHjqti3fFzrC8LXkkxxKYlLbuI/CJcwa2JN4Ppg2GK0= maunium.net/go/mautrix v0.16.3-0.20250810202616-6bc5698125c2 h1:8PdwIklPNHTL/tI9tG2S0Tf9UvAgRt8yZjJbjV0XIpA=
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c/go.mod h1:gCgLw/4c1a8QsiOWTdUdXlt5cYdE0rJ9wLeZQKPD58Q= maunium.net/go/mautrix v0.16.3-0.20250810202616-6bc5698125c2/go.mod h1:gCgLw/4c1a8QsiOWTdUdXlt5cYdE0rJ9wLeZQKPD58Q=

View File

@@ -205,6 +205,7 @@ func (guild *Guild) CreateMatrixRoom(user *User, meta *discordgo.Guild) error {
Preset: "private_chat", Preset: "private_chat",
InitialState: initialState, InitialState: initialState,
CreationContent: creationContent, CreationContent: creationContent,
RoomVersion: "11",
}) })
if err != nil { if err != nil {
guild.log.Warnln("Failed to create room:", err) guild.log.Warnln("Failed to create room:", err)

View File

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

179
portal.go
View File

@@ -13,6 +13,7 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -300,7 +301,8 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI {
type CustomBridgeInfoContent struct { type CustomBridgeInfoContent struct {
event.BridgeEventContent event.BridgeEventContent
RoomType string `json:"com.beeper.room_type,omitempty"` RoomType string `json:"com.beeper.room_type,omitempty"`
RoomTypeV2 string `json:"com.beeper.room_type.v2,omitempty"`
} }
func init() { func init() {
@@ -343,7 +345,14 @@ func (portal *Portal) getBridgeInfo() (string, CustomBridgeInfoContent) {
if portal.Type == discordgo.ChannelTypeDM || portal.Type == discordgo.ChannelTypeGroupDM { if portal.Type == discordgo.ChannelTypeDM || portal.Type == discordgo.ChannelTypeGroupDM {
roomType = "dm" roomType = "dm"
} }
return bridgeInfoStateKey, CustomBridgeInfoContent{bridgeInfo, roomType} var roomTypeV2 string
if portal.Type == discordgo.ChannelTypeDM {
roomTypeV2 = "dm"
} else if portal.Type == discordgo.ChannelTypeGroupDM {
roomTypeV2 = "group_dm"
}
return bridgeInfoStateKey, CustomBridgeInfoContent{bridgeInfo, roomType, roomTypeV2}
} }
func (portal *Portal) UpdateBridgeInfo() { func (portal *Portal) UpdateBridgeInfo() {
@@ -480,6 +489,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, channel *discordgo.Channel) e
IsDirect: portal.IsPrivateChat(), IsDirect: portal.IsPrivateChat(),
InitialState: initialState, InitialState: initialState,
CreationContent: creationContent, CreationContent: creationContent,
RoomVersion: "11",
} }
if !portal.shouldSetDMRoomMetadata() && !portal.FriendNick { if !portal.shouldSetDMRoomMetadata() && !portal.FriendNick {
req.Name = "" req.Name = ""
@@ -1512,9 +1522,15 @@ func (portal *Portal) RefererOptIfUser(sess *discordgo.Session, threadID string)
} }
func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver { if portal.IsPrivateChat() {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring") if !sender.IsLoggedIn() {
return 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) content, ok := evt.Content.Parsed.(*event.MessageEventContent)
@@ -1535,7 +1551,8 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
if editMXID := content.GetRelatesTo().GetReplaceID(); editMXID != "" && content.NewContent != nil { if editMXID := content.GetRelatesTo().GetReplaceID(); editMXID != "" && content.NewContent != nil {
edits := portal.bridge.DB.Message.GetByMXID(portal.Key, editMXID) edits := portal.bridge.DB.Message.GetByMXID(portal.Key, editMXID)
if edits != nil { 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 err error
var msg *discordgo.Message var msg *discordgo.Message
if !isWebhookSend { if !isWebhookSend {
@@ -1590,9 +1607,12 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
} }
} }
if replyToMXID := content.RelatesTo.GetNonFallbackReplyTo(); replyToMXID != "" { replyToMXID := content.RelatesTo.GetNonFallbackReplyTo()
var replyToUser id.UserID
if replyToMXID != "" {
replyTo := portal.bridge.DB.Message.GetByMXID(portal.Key, replyToMXID) replyTo := portal.bridge.DB.Message.GetByMXID(portal.Key, replyToMXID)
if replyTo != nil && replyTo.ThreadID == threadID { if replyTo != nil && replyTo.ThreadID == threadID {
replyToUser = replyTo.SenderMXID
if isWebhookSend { if isWebhookSend {
messageURL := fmt.Sprintf("https://discord.com/channels/%s/%s/%s", portal.GuildID, channelID, replyTo.DiscordID) messageURL := fmt.Sprintf("https://discord.com/channels/%s/%s/%s", portal.GuildID, channelID, replyTo.DiscordID)
embed, err := portal.convertReplyMessageToEmbed(replyTo.MXID, messageURL) embed, err := portal.convertReplyMessageToEmbed(replyTo.MXID, messageURL)
@@ -1611,7 +1631,7 @@ 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, parseAllowedLinkPreviews(evt.Content.Raw))
if content.MsgType == event.MsgEmote { if content.MsgType == event.MsgEmote {
sendReq.Content = fmt.Sprintf("_%s_", sendReq.Content) sendReq.Content = fmt.Sprintf("_%s_", sendReq.Content)
} }
@@ -1624,21 +1644,30 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
filename := content.Body filename := content.Body
if content.FileName != "" && content.FileName != content.Body { if content.FileName != "" && content.FileName != content.Body {
filename = content.FileName 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 {
filename = "SPOILER_" + filename
} }
if portal.bridge.Config.Bridge.UseDiscordCDNUpload && !isWebhookSend && sess.IsUser { if portal.bridge.Config.Bridge.UseDiscordCDNUpload && !isWebhookSend && sess.IsUser {
att := &discordgo.MessageAttachment{ att := &discordgo.MessageAttachment{
ID: "0", ID: "0",
Filename: filename, Filename: filename,
Description: description, Description: description,
OriginalContentType: content.Info.MimeType,
} }
sendReq.Attachments = []*discordgo.MessageAttachment{att} sendReq.Attachments = []*discordgo.MessageAttachment{att}
isClip := false
prep, err := sender.Session.ChannelAttachmentCreate(channelID, &discordgo.ReqPrepareAttachments{ prep, err := sender.Session.ChannelAttachmentCreate(channelID, &discordgo.ReqPrepareAttachments{
Files: []*discordgo.FilePrepare{{ Files: []*discordgo.FilePrepare{{
Size: len(data), Size: len(data),
Name: att.Filename, Name: att.Filename,
ID: sender.NextDiscordUploadID(), ID: sender.NextDiscordUploadID(),
IsClip: &isClip,
OriginalContentType: att.OriginalContentType,
}}, }},
}, portal.RefererOpt(threadID)) }, portal.RefererOpt(threadID))
if err != nil { if err != nil {
@@ -1663,10 +1692,22 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
go portal.sendMessageMetrics(evt, fmt.Errorf("%w %q", errUnknownMsgType, content.MsgType), "Ignoring") go portal.sendMessageMetrics(evt, fmt.Errorf("%w %q", errUnknownMsgType, content.MsgType), "Ignoring")
return return
} }
silentReply := content.Mentions != nil && replyToMXID != "" &&
(len(content.Mentions.UserIDs) == 0 || (replyToUser != "" && !slices.Contains(content.Mentions.UserIDs, replyToUser)))
if silentReply && sendReq.AllowedMentions != nil {
sendReq.AllowedMentions.RepliedUser = false
}
if !isWebhookSend { if !isWebhookSend {
// AllowedMentions must not be set for real users, and it's also not that useful for personal bots. // AllowedMentions must not be set for real users, and it's also not that useful for personal bots.
// It's only important for relaying, where the webhook may have higher permissions than the user on Matrix. // It's only important for relaying, where the webhook may have higher permissions than the user on Matrix.
sendReq.AllowedMentions = nil if silentReply {
sendReq.AllowedMentions = &discordgo.MessageAllowedMentions{
Parse: []discordgo.AllowedMentionType{discordgo.AllowedMentionTypeUsers, discordgo.AllowedMentionTypeRoles, discordgo.AllowedMentionTypeEveryone},
RepliedUser: false,
}
} else {
sendReq.AllowedMentions = nil
}
} else if strings.Contains(sendReq.Content, "@everyone") || strings.Contains(sendReq.Content, "@here") { } else if strings.Contains(sendReq.Content, "@everyone") || strings.Contains(sendReq.Content, "@here") {
powerLevels, err := portal.MainIntent().PowerLevels(portal.MXID) powerLevels, err := portal.MainIntent().PowerLevels(portal.MXID)
if err != nil { if err != nil {
@@ -1716,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) { func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) {
if portal.bridge.Config.Bridge.DeliveryReceipts { if portal.bridge.Config.Bridge.DeliveryReceipts {
err := portal.bridge.Bot.MarkRead(portal.MXID, eventID) err := portal.bridge.Bot.MarkRead(portal.MXID, eventID)
@@ -1866,12 +1929,13 @@ func (portal *Portal) getMatrixUsers() ([]id.UserID, error) {
} }
func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) { 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 { if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring") go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return return
} else if !sender.IsLoggedIn() {
//go portal.sendMessageMetrics(evt, errReactionUserNotLoggedIn, "Ignoring")
return
} }
reaction := evt.Content.AsReaction() reaction := evt.Content.AsReaction()
@@ -2049,9 +2113,15 @@ func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.Mess
} }
func (portal *Portal) handleMatrixRedaction(sender *User, evt *event.Event) { func (portal *Portal) handleMatrixRedaction(sender *User, evt *event.Event) {
if portal.IsPrivateChat() && sender.DiscordID != portal.Key.Receiver { if portal.IsPrivateChat() {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring") if !sender.IsLoggedIn() {
return go portal.sendMessageMetrics(evt, errUserNotLoggedIn, "Ignoring")
return
}
if sender.DiscordID != portal.Key.Receiver {
go portal.sendMessageMetrics(evt, errUserNotReceiver, "Ignoring")
return
}
} }
sess := sender.Session sess := sender.Session
@@ -2506,3 +2576,74 @@ func (portal *Portal) UpdateInfo(source *User, meta *discordgo.Channel) *discord
} }
return meta 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()
}

View File

@@ -18,6 +18,8 @@ package main
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"html" "html"
"strconv" "strconv"
@@ -32,6 +34,8 @@ import (
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database"
) )
type ConvertedMessage struct { type ConvertedMessage struct {
@@ -152,24 +156,27 @@ func (portal *Portal) convertDiscordAttachment(ctx context.Context, intent *apps
Size: att.Size, Size: att.Size,
}, },
} }
var extra = make(map[string]any)
if strings.HasPrefix(att.Filename, "SPOILER_") {
extra["page.codeberg.everypizza.msc4193.spoiler"] = true
}
if att.Description != "" { if att.Description != "" {
content.Body = att.Description content.Body = att.Description
content.FileName = att.Filename content.FileName = att.Filename
} }
var extra map[string]any
switch strings.ToLower(strings.Split(att.ContentType, "/")[0]) { switch strings.ToLower(strings.Split(att.ContentType, "/")[0]) {
case "audio": case "audio":
content.MsgType = event.MsgAudio content.MsgType = event.MsgAudio
if att.Waveform != nil { if att.Waveform != nil {
// TODO convert waveform // TODO convert waveform
extra = map[string]any{ extra["org.matrix.msc1767.audio"] = map[string]any{
"org.matrix.1767.audio": map[string]any{ "duration": int(att.DurationSeconds * 1000),
"duration": int(att.DurationSeconds * 1000),
},
"org.matrix.msc3245.voice": map[string]any{},
} }
extra["org.matrix.msc3245.voice"] = map[string]any{}
} }
case "image": case "image":
content.MsgType = event.MsgImage content.MsgType = event.MsgImage
@@ -342,10 +349,10 @@ func (puppet *Puppet) addMemberMeta(part *ConvertedMessage, msg *discordgo.Messa
var discordAvatarURL string var discordAvatarURL string
if msg.Member.Avatar != "" { if msg.Member.Avatar != "" {
var err error var err error
avatarURL, discordAvatarURL, 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.Member.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.Member.Avatar).
Msg("Failed to reupload guild user avatar") Msg("Failed to reupload guild user avatar")
} }
} }
@@ -357,8 +364,7 @@ func (puppet *Puppet) addMemberMeta(part *ConvertedMessage, msg *discordgo.Messa
} }
if msg.Member.Nick != "" || !avatarURL.IsEmpty() { if msg.Member.Nick != "" || !avatarURL.IsEmpty() {
perMessageProfile := map[string]any{ perMessageProfile := map[string]any{
"is_multiple_users": false, "id": fmt.Sprintf("%s_%s", msg.GuildID, msg.Author.ID),
"displayname": msg.Member.Nick, "displayname": msg.Member.Nick,
"avatar_url": avatarURL.String(), "avatar_url": avatarURL.String(),
} }
@@ -396,11 +402,21 @@ func (puppet *Puppet) addWebhookMeta(part *ConvertedMessage, msg *discordgo.Mess
"avatar_url": msg.Author.AvatarURL(""), "avatar_url": msg.Author.AvatarURL(""),
"avatar_mxc": avatarURL.String(), "avatar_mxc": avatarURL.String(),
} }
profileID := sha256.Sum256(fmt.Appendf(nil, "%s:%s", msg.Author.Username, msg.Author.Avatar))
hasFallback := false
if msg.ApplicationID == "" &&
puppet.bridge.Config.Bridge.PrefixWebhookMessages &&
(part.Content.MsgType == event.MsgText || part.Content.MsgType == event.MsgNotice || (part.Content.FileName != "" && part.Content.FileName != part.Content.Body)) {
part.Content.EnsureHasHTML()
part.Content.Body = fmt.Sprintf("%s: %s", msg.Author.Username, part.Content.Body)
part.Content.FormattedBody = fmt.Sprintf("<strong data-mx-profile-fallback>%s: </strong>%s", html.EscapeString(msg.Author.Username), part.Content.FormattedBody)
hasFallback = true
}
part.Extra["com.beeper.per_message_profile"] = map[string]any{ part.Extra["com.beeper.per_message_profile"] = map[string]any{
"is_multiple_users": true, "id": hex.EncodeToString(profileID[:]),
"avatar_url": avatarURL.String(),
"avatar_url": avatarURL.String(), "displayname": msg.Author.Username,
"displayname": msg.Author.Username, "has_fallback": hasFallback,
} }
} }
@@ -633,7 +649,7 @@ func isPlainGifMessage(msg *discordgo.Message) bool {
} }
embed := msg.Embeds[0] embed := msg.Embeds[0]
isGifVideo := embed.Type == discordgo.EmbedTypeGifv && embed.Video != nil isGifVideo := embed.Type == discordgo.EmbedTypeGifv && embed.Video != nil
isGifImage := embed.Type == discordgo.EmbedTypeImage && embed.Image == nil && embed.Thumbnail != nil isGifImage := embed.Type == discordgo.EmbedTypeImage && embed.Image == nil && embed.Thumbnail != nil && embed.Title == ""
contentIsOnlyURL := msg.Content == embed.URL || discordLinkRegexFull.MatchString(msg.Content) contentIsOnlyURL := msg.Content == embed.URL || discordLinkRegexFull.MatchString(msg.Content)
return contentIsOnlyURL && (isGifVideo || isGifImage) return contentIsOnlyURL && (isGifVideo || isGifImage)
} }
@@ -660,6 +676,12 @@ func (portal *Portal) convertDiscordMentions(msg *discordgo.Message, syncGhosts
return &matrixMentions return &matrixMentions
} }
const forwardTemplateHTML = `<blockquote>
<p>↷ Forwarded</p>
%s
<p>%s</p>
</blockquote>`
func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage { func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
if msg.Type == discordgo.MessageTypeCall { if msg.Type == discordgo.MessageTypeCall {
@@ -681,6 +703,36 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
} }
if msg.Content != "" && !isPlainGifMessage(msg) { if msg.Content != "" && !isPlainGifMessage(msg) {
htmlParts = append(htmlParts, portal.renderDiscordMarkdownOnlyHTML(msg.Content, true)) htmlParts = append(htmlParts, portal.renderDiscordMarkdownOnlyHTML(msg.Content, true))
} else if msg.MessageReference != nil &&
msg.MessageReference.Type == discordgo.MessageReferenceTypeForward &&
len(msg.MessageSnapshots) > 0 &&
msg.MessageSnapshots[0].Message != nil {
forwardedHTML := portal.renderDiscordMarkdownOnlyHTMLNoUnwrap(msg.MessageSnapshots[0].Message.Content, true)
msgTSText := msg.MessageSnapshots[0].Message.Timestamp.Format("2006-01-02 15:04 MST")
origLink := fmt.Sprintf("unknown channel • %s", msgTSText)
forwardedFromPortal := portal.bridge.GetExistingPortalByID(database.NewPortalKey(msg.MessageReference.ChannelID, ""))
if forwardedFromPortal != nil {
origMessage := portal.bridge.DB.Message.GetFirstByDiscordID(forwardedFromPortal.Key, msg.MessageReference.MessageID)
if origMessage != nil {
origLink = fmt.Sprintf(
`<a href="%s">#%s • %s</a>`,
forwardedFromPortal.MXID.EventURI(origMessage.MXID, portal.bridge.AS.HomeserverDomain),
forwardedFromPortal.PlainName,
msgTSText,
)
} else if forwardedFromPortal.MXID != "" {
origLink = fmt.Sprintf(
`<a href="%s">#%s</a> • %s`,
forwardedFromPortal.MXID.URI(portal.bridge.AS.HomeserverDomain),
forwardedFromPortal.PlainName,
msgTSText,
)
} else if forwardedFromPortal.PlainName != "" {
origLink = fmt.Sprintf("%s • %s", forwardedFromPortal.PlainName, msgTSText)
}
}
htmlParts = append(htmlParts, fmt.Sprintf(forwardTemplateHTML, forwardedHTML, origLink))
} }
previews := make([]*BeeperLinkPreview, 0) previews := make([]*BeeperLinkPreview, 0)
for i, embed := range msg.Embeds { for i, embed := range msg.Embeds {
@@ -723,11 +775,5 @@ func (portal *Portal) convertDiscordTextMessage(ctx context.Context, intent *app
"com.beeper.linkpreviews": previews, "com.beeper.linkpreviews": previews,
} }
if msg.WebhookID != "" && msg.ApplicationID == "" && portal.bridge.Config.Bridge.PrefixWebhookMessages {
content.EnsureHasHTML()
content.Body = fmt.Sprintf("%s: %s", msg.Author.Username, content.Body)
content.FormattedBody = fmt.Sprintf("<strong>%s</strong>: %s", html.EscapeString(msg.Author.Username), content.FormattedBody)
}
return &ConvertedMessage{Type: event.EventMessage, Content: &content, Extra: extraContent} return &ConvertedMessage{Type: event.EventMessage, Content: &content, Extra: extraContent}
} }

29
user.go
View File

@@ -344,6 +344,7 @@ func (user *User) getSpaceRoom(ptr *id.RoomID, name, topic string, parent id.Roo
user.MXID: 50, user.MXID: 50,
}, },
}, },
RoomVersion: "11",
}) })
if err != nil { if err != nil {
@@ -549,6 +550,17 @@ func (user *User) Connect() error {
if err != nil { if err != nil {
return err 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 != "" { if user.bridge.Config.Bridge.Proxy != "" {
u, _ := url.Parse(user.bridge.Config.Bridge.Proxy) u, _ := url.Parse(user.bridge.Config.Bridge.Proxy)
tlsConf := &tls.Config{ tlsConf := &tls.Config{
@@ -568,7 +580,10 @@ func (user *User) Connect() error {
} else { } else {
session.LogLevel = discordgo.LogInformational 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{}) { 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 userDiscordLog.WithLevel(discordToZeroLevel(msgL)).Caller(caller+1).Msgf(strings.TrimSpace(format), a...) // zerolog-allow-msgf
} }
@@ -577,9 +592,11 @@ func (user *User) Connect() error {
} }
session.EventHandler = user.eventHandlerSync session.EventHandler = user.eventHandlerSync
err = session.LoadMainPage(context.TODO()) if session.IsUser {
if err != nil { err = session.LoadMainPage(context.TODO())
user.log.Warn().Err(err).Msg("Failed to load main page") if err != nil {
user.log.Warn().Err(err).Msg("Failed to load main page")
}
} }
user.Session = session user.Session = session
@@ -804,6 +821,7 @@ func (user *User) subscribeGuilds(delay time.Duration) {
func (user *User) resumeHandler(_ *discordgo.Resumed) { func (user *User) resumeHandler(_ *discordgo.Resumed) {
user.log.Debug().Msg("Discord connection resumed") user.log.Debug().Msg("Discord connection resumed")
user.subscribeGuilds(0 * time.Second) user.subscribeGuilds(0 * time.Second)
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
} }
func (user *User) addPrivateChannelToSpace(portal *Portal) bool { func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
@@ -1004,7 +1022,6 @@ func (user *User) connectedHandler(_ *discordgo.Connect) {
user.log.Debug().Msg("Connected to Discord") user.log.Debug().Msg("Connected to Discord")
if user.wasDisconnected { if user.wasDisconnected {
user.wasDisconnected = false user.wasDisconnected = false
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
} }
} }
@@ -1151,7 +1168,7 @@ func (user *User) channelUpdateHandler(c *discordgo.ChannelUpdate) {
portal := user.GetPortalByMeta(c.Channel) portal := user.GetPortalByMeta(c.Channel)
if c.GuildID == "" { if c.GuildID == "" {
user.handlePrivateChannel(portal, c.Channel, time.Now(), true, user.IsInSpace(portal.Key.String())) user.handlePrivateChannel(portal, c.Channel, time.Now(), true, user.IsInSpace(portal.Key.String()))
} else { } else if user.channelIsBridgeable(c.Channel) {
portal.UpdateInfo(user, c.Channel) portal.UpdateInfo(user, c.Channel)
} }
} }