Compare commits

..

1 commit

Author SHA1 Message Date
Tulir Asokan
b84e7bb752 wip
[skip ci]
2024-04-19 00:00:21 +03:00
31 changed files with 528 additions and 598 deletions

View file

@ -1,14 +1,6 @@
# v0.7.0 (2024-07-16)
# v0.7.0 (unreleased)
* Bumped minimum Go version to 1.21.
* Added support for Matrix v1.11 authenticated media.
* This also changes how avatars are sent to Discord when using relay webhooks.
To keep avatars working, you must configure `public_address` in the *bridge*
section of the config and proxy `/mautrix-discord/avatar/*` from that
address to the bridge.
* Added `create-portal` command to create individual portals bypassing the
bridging mode. When used in combination with the `if-portal-exists` bridging
mode, this can be used to bridge individual channels from a guild.
* Changed how direct media access works to make it compatible with Discord's
signed URL requirement. The new system must be enabled manually, see
[docs](https://docs.mau.fi/bridges/go/discord/direct-media.html) for info.

View file

@ -12,8 +12,7 @@ All setup and usage instructions are located on [docs.mau.fi]. Some quick links:
[Relaying with webhooks](https://docs.mau.fi/bridges/go/discord/relay.html)
### Features & Roadmap
[ROADMAP.md](https://github.com/mautrix/discord/blob/main/ROADMAP.md)
contains a general overview of what is supported by the bridge.
[ROADMAP.md](ROADMAP.md) contains a general overview of what is supported by the bridge.
## Discussion
Matrix room: [#discord:maunium.net](https://matrix.to/#/#discord:maunium.net)

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (

View file

@ -62,7 +62,6 @@ func (br *DiscordBridge) RegisterCommands() {
cmdBridge,
cmdUnbridge,
cmdDeletePortal,
cmdCreatePortal,
cmdSetRelay,
cmdUnsetRelay,
cmdGuilds,
@ -758,7 +757,7 @@ func fnBridge(ce *WrappedCommandEvent) {
portal.updateRoomName()
portal.updateRoomAvatar()
portal.updateRoomTopic()
portal.updateSpace(ce.User)
portal.updateSpace()
portal.UpdateBridgeInfo()
state, err := portal.MainIntent().State(portal.MXID)
if err != nil {
@ -786,45 +785,6 @@ var cmdUnbridge = &commands.FullHandler{
RequiresEventLevel: roomModerator,
}
var cmdCreatePortal = &commands.FullHandler{
Func: wrapCommand(fnCreatePortal),
Name: "create-portal",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Create a portal for a specific channel",
Args: "<_channel ID_>",
},
RequiresLogin: true,
}
func fnCreatePortal(ce *WrappedCommandEvent) {
meta, err := ce.User.Session.Channel(ce.Args[0])
if err != nil {
ce.Reply("Failed to get channel info: %v", err)
return
} else if meta == nil {
ce.Reply("Channel not found")
return
} else if !ce.User.channelIsBridgeable(meta) {
ce.Reply("That channel can't be bridged")
return
}
portal := ce.User.GetPortalByMeta(meta)
if portal.Guild != nil && portal.Guild.BridgingMode == database.GuildBridgeNothing {
ce.Reply("That guild is set to not bridge any messages. Bridge the guild with `$cmdprefix guilds bridge %s` first", portal.Guild.ID)
return
} else if portal.MXID != "" {
ce.Reply("That channel is already bridged: [%s](%s)", portal.Name, portal.MXID.URI(portal.bridge.Config.Homeserver.Domain).MatrixToURL())
return
}
err = portal.CreateMatrixRoom(ce.User, meta)
if err != nil {
ce.Reply("Failed to create portal: %v", err)
} else {
ce.Reply("Portal created: [%s](%s)", portal.Name, portal.MXID.URI(portal.bridge.Config.Homeserver.Domain).MatrixToURL())
}
}
var cmdDeletePortal = &commands.FullHandler{
Func: wrapCommand(fnUnbridge),
Name: "delete-portal",

View file

@ -37,9 +37,6 @@ type BridgeConfig struct {
PortalMessageBuffer int `yaml:"portal_message_buffer"`
PublicAddress string `yaml:"public_address"`
AvatarProxyKey string `yaml:"avatar_proxy_key"`
DeliveryReceipts bool `yaml:"delivery_receipts"`
MessageStatusEvents bool `yaml:"message_status_events"`
MessageErrorNotices bool `yaml:"message_error_notices"`

View file

@ -26,6 +26,8 @@ import (
func DoUpgrade(helper *up.Helper) {
bridgeconfig.Upgrader.DoUpgrade(helper)
helper.Copy(up.Str|up.Null, "homeserver", "public_address")
helper.Copy(up.Str, "bridge", "username_template")
helper.Copy(up.Str, "bridge", "displayname_template")
helper.Copy(up.Str, "bridge", "channel_name_template")
@ -40,12 +42,6 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Str, "bridge", "private_chat_portal_meta")
}
helper.Copy(up.Int, "bridge", "startup_private_channel_create_limit")
helper.Copy(up.Str|up.Null, "bridge", "public_address")
if apkey, ok := helper.Get(up.Str, "bridge", "avatar_proxy_key"); !ok || apkey == "generate" {
helper.Set(up.Str, random.String(32), "bridge", "avatar_proxy_key")
} else {
helper.Copy(up.Str, "bridge", "avatar_proxy_key")
}
helper.Copy(up.Int, "bridge", "portal_message_buffer")
helper.Copy(up.Bool, "bridge", "delivery_receipts")
helper.Copy(up.Bool, "bridge", "message_status_events")

View file

@ -1,6 +1,24 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"context"
"maunium.net/go/mautrix/id"
)
@ -33,7 +51,7 @@ func (puppet *Puppet) ClearCustomMXID() {
}
func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
newIntent, newAccessToken, err := puppet.bridge.DoublePuppet.Setup(puppet.CustomMXID, puppet.AccessToken, reloginOnFail)
newIntent, newAccessToken, err := puppet.bridge.DoublePuppet.Setup(context.TODO(), puppet.CustomMXID, puppet.AccessToken, reloginOnFail)
if err != nil {
puppet.ClearCustomMXID()
return err

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
@ -6,7 +22,6 @@ import (
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"go.mau.fi/util/dbutil"
"maunium.net/go/maulogger/v2"
"go.mau.fi/mautrix-discord/database/upgrades"
)
@ -25,52 +40,18 @@ type Database struct {
File *FileQuery
}
func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
db := &Database{Database: baseDB}
func New(db *dbutil.Database) *Database {
db.UpgradeTable = upgrades.Table
db.User = &UserQuery{
db: db,
log: log.Sub("User"),
return &Database{
Database: db,
User: &UserQuery{dbutil.MakeQueryHelper(db, newUser)},
Portal: &PortalQuery{dbutil.MakeQueryHelper(db, newPortal)},
Puppet: &PuppetQuery{dbutil.MakeQueryHelper(db, newPuppet)},
Message: &MessageQuery{dbutil.MakeQueryHelper(db, newMessage)},
Thread: &ThreadQuery{dbutil.MakeQueryHelper(db, newThread)},
Reaction: &ReactionQuery{dbutil.MakeQueryHelper(db, newReaction)},
Guild: &GuildQuery{dbutil.MakeQueryHelper(db, newGuild)},
Role: &RoleQuery{dbutil.MakeQueryHelper(db, newRole)},
File: &FileQuery{dbutil.MakeQueryHelper(db, newFile)},
}
db.Portal = &PortalQuery{
db: db,
log: log.Sub("Portal"),
}
db.Puppet = &PuppetQuery{
db: db,
log: log.Sub("Puppet"),
}
db.Message = &MessageQuery{
db: db,
log: log.Sub("Message"),
}
db.Thread = &ThreadQuery{
db: db,
log: log.Sub("Thread"),
}
db.Reaction = &ReactionQuery{
db: db,
log: log.Sub("Reaction"),
}
db.Guild = &GuildQuery{
db: db,
log: log.Sub("Guild"),
}
db.Role = &RoleQuery{
db: db,
log: log.Sub("Role"),
}
db.File = &FileQuery{
db: db,
log: log.Sub("File"),
}
return db
}
func strPtr[T ~string](val T) *string {
if val == "" {
return nil
}
valStr := string(val)
return &valStr
}

View file

@ -1,51 +1,66 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"context"
"database/sql"
"encoding/json"
"errors"
"time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/id"
)
type FileQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*File]
}
// language=postgresql
const (
fileSelect = "SELECT url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp FROM discord_file"
fileInsert = `
getFileByURLQuery = `
SELECT url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp
FROM discord_file WHERE url=$1 AND encrypted=$2
`
getFileByEmojiMXCQuery = `
SELECT url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp
FROM discord_file WHERE mxc=$1 AND emoji_name<>'' LIMIT 1
`
insertFileQuery = `
INSERT INTO discord_file (url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
deleteFileQuery = "DELETE FROM discord_file WHERE url=$1 AND encrypted=$2"
)
func (fq *FileQuery) New() *File {
return &File{
db: fq.db,
log: fq.log,
}
func newFile(qh *dbutil.QueryHelper[*File]) *File {
return &File{qh: qh}
}
func (fq *FileQuery) Get(url string, encrypted bool) *File {
query := fileSelect + " WHERE url=$1 AND encrypted=$2"
return fq.New().Scan(fq.db.QueryRow(query, url, encrypted))
func (fq *FileQuery) Get(ctx context.Context, url string, encrypted bool) (*File, error) {
return fq.QueryOne(ctx, getFileByURLQuery, url, encrypted)
}
func (fq *FileQuery) GetEmojiByMXC(mxc id.ContentURI) *File {
query := fileSelect + " WHERE mxc=$1 AND emoji_name<>'' LIMIT 1"
return fq.New().Scan(fq.db.QueryRow(query, mxc.String()))
func (fq *FileQuery) GetEmojiByMXC(ctx context.Context, mxc id.ContentURI) (*File, error) {
return fq.QueryOne(ctx, getFileByEmojiMXCQuery, mxc.String())
}
type File struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*File]
URL string
Encrypted bool
@ -63,76 +78,38 @@ type File struct {
Timestamp time.Time
}
func (f *File) Scan(row dbutil.Scannable) *File {
var fileID, emojiName, decryptionInfo sql.NullString
func (f *File) Scan(row dbutil.Scannable) (*File, error) {
var fileID, emojiName sql.NullString
var width, height sql.NullInt32
var timestamp int64
var mxc string
err := row.Scan(&f.URL, &f.Encrypted, &mxc, &fileID, &emojiName, &f.Size, &width, &height, &f.MimeType, &decryptionInfo, &timestamp)
err := row.Scan(
&f.URL, &f.Encrypted, &f.MXC, &fileID, &emojiName, &f.Size,
&width, &height, &f.MimeType,
dbutil.JSON{Data: &f.DecryptionInfo}, &timestamp,
)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
f.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
f.ID = fileID.String
f.EmojiName = emojiName.String
f.Timestamp = time.UnixMilli(timestamp).UTC()
f.Width = int(width.Int32)
f.Height = int(height.Int32)
f.MXC, err = id.ParseContentURI(mxc)
if err != nil {
f.log.Errorfln("Failed to parse content URI %s: %v", mxc, err)
panic(err)
}
if decryptionInfo.Valid {
err = json.Unmarshal([]byte(decryptionInfo.String), &f.DecryptionInfo)
if err != nil {
f.log.Errorfln("Failed to unmarshal decryption info of %v: %v", f.MXC, err)
panic(err)
}
}
return f
return f, nil
}
func positiveIntToNullInt32(val int) (ptr sql.NullInt32) {
if val > 0 {
ptr.Valid = true
ptr.Int32 = int32(val)
}
return
}
func (f *File) Insert(txn dbutil.Execable) {
if txn == nil {
txn = f.db
}
var decryptionInfoStr sql.NullString
if f.DecryptionInfo != nil {
decryptionInfo, err := json.Marshal(f.DecryptionInfo)
if err != nil {
f.log.Warnfln("Failed to marshal decryption info of %v: %v", f.MXC, err)
panic(err)
}
decryptionInfoStr.Valid = true
decryptionInfoStr.String = string(decryptionInfo)
}
_, err := txn.Exec(fileInsert,
f.URL, f.Encrypted, f.MXC.String(), strPtr(f.ID), strPtr(f.EmojiName), f.Size,
positiveIntToNullInt32(f.Width), positiveIntToNullInt32(f.Height), f.MimeType,
decryptionInfoStr, f.Timestamp.UnixMilli(),
)
if err != nil {
f.log.Warnfln("Failed to insert copied file %v: %v", f.MXC, err)
panic(err)
func (f *File) sqlVariables() []any {
return []any{
f.URL, f.Encrypted, f.MXC.String(), dbutil.StrPtr(f.ID), dbutil.StrPtr(f.EmojiName), f.Size,
dbutil.NumPtr(f.Width), dbutil.NumPtr(f.Height), f.MimeType,
dbutil.JSONPtr(f.DecryptionInfo), f.Timestamp.UnixMilli(),
}
}
func (f *File) Delete() {
_, err := f.db.Exec("DELETE FROM discord_file WHERE url=$1 AND encrypted=$2", f.URL, f.Encrypted)
if err != nil {
f.log.Warnfln("Failed to delete copied file %v: %v", f.MXC, err)
panic(err)
}
func (f *File) Insert(ctx context.Context) error {
return f.qh.Exec(ctx, insertFileQuery, f.sqlVariables()...)
}
func (f *File) Delete(ctx context.Context) error {
return f.qh.Exec(ctx, deleteFileQuery, f.URL, f.Encrypted)
}

View file

@ -1,13 +1,27 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"errors"
"fmt"
"strings"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
@ -75,19 +89,15 @@ func (gbm GuildBridgingMode) Description() string {
}
type GuildQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*Guild]
}
const (
guildSelect = "SELECT dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, bridging_mode FROM guild"
)
func (gq *GuildQuery) New() *Guild {
return &Guild{
db: gq.db,
log: gq.log,
}
func newGuild(qh *dbutil.QueryHelper[*Guild]) *Guild {
return &Guild{qh: qh}
}
func (gq *GuildQuery) GetByID(dcid string) *Guild {
@ -119,8 +129,7 @@ func (gq *GuildQuery) GetAll() []*Guild {
}
type Guild struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Guild]
ID string
MXID id.RoomID
@ -134,24 +143,19 @@ type Guild struct {
BridgingMode GuildBridgingMode
}
func (g *Guild) Scan(row dbutil.Scannable) *Guild {
func (g *Guild) Scan(row dbutil.Scannable) (*Guild, error) {
var mxid sql.NullString
var avatarURL string
err := row.Scan(&g.ID, &mxid, &g.PlainName, &g.Name, &g.NameSet, &g.Avatar, &avatarURL, &g.AvatarSet, &g.BridgingMode)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
g.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
if g.BridgingMode < GuildBridgeNothing || g.BridgingMode > GuildBridgeEverything {
panic(fmt.Errorf("invalid guild bridging mode %d in guild %s", g.BridgingMode, g.ID))
}
g.MXID = id.RoomID(mxid.String)
g.AvatarURL, _ = id.ParseContentURI(avatarURL)
return g
return g, nil
}
func (g *Guild) mxidPtr() *id.RoomID {

View file

@ -1,97 +1,92 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"errors"
"context"
"fmt"
"strings"
"time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type MessageQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*Message]
}
const (
messageSelect = "SELECT dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp, dc_edit_timestamp, dc_thread_id, mxid, sender_mxid FROM message"
getMessageBaseQuery = `
SELECT dcid, dc_attachment_id, dc_chan_id, dc_chan_receiver, dc_sender, timestamp,
dc_edit_timestamp, dc_thread_id, mxid, sender_mxid
FROM message
`
getMessageByDiscordIDQuery = getMessageBaseQuery +
"WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id"
getFirstMessageByDiscordIDQuery = getMessageByDiscordIDQuery +
" LIMIT 1"
getLastMessageByDiscordIDQuery = getMessageByDiscordIDQuery +
" DESC LIMIT 1"
getClosestMessageBeforeTimeQuery = getMessageBaseQuery +
"WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 AND timestamp<=$4 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1"
getLastMessageInThreadQuery = getMessageBaseQuery +
" WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1"
getLastMessageInPortalQuery = getMessageBaseQuery +
" WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 ORDER BY timestamp DESC LIMIT 1"
getMessageByMXIDQuery = getMessageBaseQuery +
" WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND mxid=$3"
deleteAllMessagesInPortalQuery = "DELETE FROM message WHERE dc_chan_id=$1 AND dc_chan_receiver=$2"
)
func (mq *MessageQuery) New() *Message {
return &Message{
db: mq.db,
log: mq.log,
}
func newMessage(qh *dbutil.QueryHelper[*Message]) *Message {
return &Message{qh: qh}
}
func (mq *MessageQuery) scanAll(rows dbutil.Rows, err error) []*Message {
if err != nil {
mq.log.Warnfln("Failed to query many messages: %v", err)
panic(err)
} else if rows == nil {
return nil
}
var messages []*Message
for rows.Next() {
messages = append(messages, mq.New().Scan(rows))
}
return messages
func (mq *MessageQuery) GetByDiscordID(ctx context.Context, key PortalKey, discordID string) ([]*Message, error) {
return mq.QueryMany(ctx, getMessageByDiscordIDQuery, key.ChannelID, key.Receiver, discordID)
}
func (mq *MessageQuery) GetByDiscordID(key PortalKey, discordID string) []*Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id ASC"
return mq.scanAll(mq.db.Query(query, key.ChannelID, key.Receiver, discordID))
func (mq *MessageQuery) GetFirstByDiscordID(ctx context.Context, key PortalKey, discordID string) (*Message, error) {
return mq.QueryOne(ctx, getFirstMessageByDiscordIDQuery, key.ChannelID, key.Receiver, discordID)
}
func (mq *MessageQuery) GetFirstByDiscordID(key PortalKey, discordID string) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id ASC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID))
func (mq *MessageQuery) GetLastByDiscordID(ctx context.Context, key PortalKey, discordID string) (*Message, error) {
return mq.QueryOne(ctx, getLastMessageByDiscordIDQuery, key.ChannelID, key.Receiver, discordID)
}
func (mq *MessageQuery) GetLastByDiscordID(key PortalKey, discordID string) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 ORDER BY dc_attachment_id DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID))
func (mq *MessageQuery) GetClosestBefore(ctx context.Context, key PortalKey, threadID string, ts time.Time) (*Message, error) {
return mq.QueryOne(ctx, getClosestMessageBeforeTimeQuery, key.ChannelID, key.Receiver, threadID, ts.UnixMilli())
}
func (mq *MessageQuery) GetClosestBefore(key PortalKey, threadID string, ts time.Time) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 AND timestamp<=$4 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, threadID, ts.UnixMilli()))
func (mq *MessageQuery) GetLastInThread(ctx context.Context, key PortalKey, threadID string) (*Message, error) {
return mq.QueryOne(ctx, getLastMessageInThreadQuery, key.ChannelID, key.Receiver, threadID)
}
func (mq *MessageQuery) GetLastInThread(key PortalKey, threadID string) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, threadID))
func (mq *MessageQuery) GetLast(ctx context.Context, key PortalKey) (*Message, error) {
return mq.QueryOne(ctx, getLastMessageInPortalQuery, key.ChannelID, key.Receiver)
}
func (mq *MessageQuery) GetLast(key PortalKey) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 ORDER BY timestamp DESC LIMIT 1"
return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver))
func (mq *MessageQuery) GetByMXID(ctx context.Context, key PortalKey, mxid id.EventID) (*Message, error) {
return mq.QueryOne(ctx, getMessageByMXIDQuery, key.ChannelID, key.Receiver, mxid)
}
func (mq *MessageQuery) DeleteAll(key PortalKey) {
query := "DELETE FROM message WHERE dc_chan_id=$1 AND dc_chan_receiver=$2"
_, err := mq.db.Exec(query, key.ChannelID, key.Receiver)
if err != nil {
mq.log.Warnfln("Failed to delete messages of %s: %v", key, err)
panic(err)
}
}
func (mq *MessageQuery) GetByMXID(key PortalKey, mxid id.EventID) *Message {
query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND mxid=$3"
row := mq.db.QueryRow(query, key.ChannelID, key.Receiver, mxid)
if row == nil {
return nil
}
return mq.New().Scan(row)
func (mq *MessageQuery) DeleteAll(ctx context.Context, key PortalKey) error {
return mq.Exec(ctx, deleteAllMessagesInPortalQuery, key.ChannelID, key.Receiver)
}
func (mq *MessageQuery) MassInsert(key PortalKey, msgs []Message) {
@ -126,8 +121,7 @@ func (mq *MessageQuery) MassInsert(key PortalKey, msgs []Message) {
}
type Message struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Message]
DiscordID string
AttachmentID string
@ -149,17 +143,11 @@ func (m *Message) DiscordProtoChannelID() string {
}
}
func (m *Message) Scan(row dbutil.Scannable) *Message {
func (m *Message) Scan(row dbutil.Scannable) (*Message, error) {
var ts, editTS int64
err := row.Scan(&m.DiscordID, &m.AttachmentID, &m.Channel.ChannelID, &m.Channel.Receiver, &m.SenderID, &ts, &editTS, &m.ThreadID, &m.MXID, &m.SenderMXID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
m.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
if ts != 0 {
@ -169,7 +157,7 @@ func (m *Message) Scan(row dbutil.Scannable) *Message {
m.EditTimestamp = time.Unix(0, editTS).UTC()
}
return m
return m, nil
}
const messageInsertQuery = `

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
@ -5,7 +21,6 @@ import (
"github.com/bwmarrin/discordgo"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
@ -39,15 +54,11 @@ func (key PortalKey) String() string {
}
type PortalQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*Portal]
}
func (pq *PortalQuery) New() *Portal {
return &Portal{
db: pq.db,
log: pq.log,
}
func newPortal(qh *dbutil.QueryHelper[*Portal]) *Portal {
return &Portal{qh: qh}
}
func (pq *PortalQuery) GetAll() []*Portal {
@ -100,8 +111,7 @@ func (pq *PortalQuery) get(query string, args ...interface{}) *Portal {
}
type Portal struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Portal]
Key PortalKey
Type discordgo.ChannelType
@ -129,22 +139,15 @@ type Portal struct {
RelayWebhookSecret string
}
func (p *Portal) Scan(row dbutil.Scannable) *Portal {
func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
var otherUserID, guildID, parentID, mxid, firstEventID, relayWebhookID, relayWebhookSecret sql.NullString
var chanType int32
var avatarURL string
err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &chanType, &otherUserID, &guildID, &parentID,
&mxid, &p.PlainName, &p.Name, &p.NameSet, &p.FriendNick, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet,
&p.Encrypted, &p.InSpace, &firstEventID, &relayWebhookID, &relayWebhookSecret)
if err != nil {
if err != sql.ErrNoRows {
p.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
p.MXID = id.RoomID(mxid.String)
@ -156,8 +159,7 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal {
p.AvatarURL, _ = id.ParseContentURI(avatarURL)
p.RelayWebhookID = relayWebhookID.String
p.RelayWebhookSecret = relayWebhookSecret.String
return p
return p, nil
}
func (p *Portal) Insert() {

View file

@ -1,10 +1,25 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
@ -15,15 +30,11 @@ const (
)
type PuppetQuery struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Puppet]
}
func (pq *PuppetQuery) New() *Puppet {
return &Puppet{
db: pq.db,
log: pq.log,
}
func newPuppet(qh *dbutil.QueryHelper[*Puppet]) *Puppet {
return &Puppet{qh: qh}
}
func (pq *PuppetQuery) Get(id string) *Puppet {
@ -62,8 +73,7 @@ func (pq *PuppetQuery) getAll(query string, args ...interface{}) []*Puppet {
}
type Puppet struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Puppet]
ID string
Name string
@ -86,28 +96,20 @@ type Puppet struct {
NextBatch string
}
func (p *Puppet) Scan(row dbutil.Scannable) *Puppet {
func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) {
var avatarURL string
var customMXID, accessToken, nextBatch sql.NullString
err := row.Scan(&p.ID, &p.Name, &p.NameSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.ContactInfoSet,
&p.GlobalName, &p.Username, &p.Discriminator, &p.IsBot, &p.IsWebhook, &p.IsApplication, &customMXID, &accessToken, &nextBatch)
if err != nil {
if err != sql.ErrNoRows {
p.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
p.AvatarURL, _ = id.ParseContentURI(avatarURL)
p.CustomMXID = id.UserID(customMXID.String)
p.AccessToken = accessToken.String
p.NextBatch = nextBatch.String
return p
return p, nil
}
func (p *Puppet) Insert() {

View file

@ -1,28 +1,36 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"errors"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type ReactionQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*Reaction]
}
const (
reactionSelect = "SELECT dc_chan_id, dc_chan_receiver, dc_msg_id, dc_sender, dc_emoji_name, dc_thread_id, mxid FROM reaction"
)
func (rq *ReactionQuery) New() *Reaction {
return &Reaction{
db: rq.db,
log: rq.log,
}
func newReaction(qh *dbutil.QueryHelper[*Reaction]) *Reaction {
return &Reaction{qh: qh}
}
func (rq *ReactionQuery) GetAllForMessage(key PortalKey, discordMessageID string) []*Reaction {
@ -67,8 +75,7 @@ func (rq *ReactionQuery) get(query string, args ...interface{}) *Reaction {
}
type Reaction struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Reaction]
Channel PortalKey
MessageID string
@ -81,17 +88,8 @@ type Reaction struct {
FirstAttachmentID string
}
func (r *Reaction) Scan(row dbutil.Scannable) *Reaction {
err := row.Scan(&r.Channel.ChannelID, &r.Channel.Receiver, &r.MessageID, &r.Sender, &r.EmojiName, &r.ThreadID, &r.MXID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
r.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
}
return r
func (r *Reaction) Scan(row dbutil.Scannable) (*Reaction, error) {
return dbutil.ValueOrErr(r, row.Scan(&r.Channel.ChannelID, &r.Channel.Receiver, &r.MessageID, &r.Sender, &r.EmojiName, &r.ThreadID, &r.MXID))
}
func (r *Reaction) DiscordProtoChannelID() string {

View file

@ -1,17 +1,30 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"errors"
"github.com/bwmarrin/discordgo"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
)
type RoleQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*Role]
}
// language=postgresql
@ -27,11 +40,8 @@ const (
roleDelete = "DELETE FROM role WHERE dc_guild_id=$1 AND dcid=$2"
)
func (rq *RoleQuery) New() *Role {
return &Role{
db: rq.db,
log: rq.log,
}
func newRole(qh *dbutil.QueryHelper[*Role]) *Role {
return &Role{qh: qh}
}
func (rq *RoleQuery) GetByID(guildID, dcid string) *Role {
@ -66,27 +76,21 @@ func (rq *RoleQuery) GetAll(guildID string) []*Role {
}
type Role struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Role]
GuildID string
discordgo.Role
}
func (r *Role) Scan(row dbutil.Scannable) *Role {
func (r *Role) Scan(row dbutil.Scannable) (*Role, error) {
var icon sql.NullString
err := row.Scan(&r.GuildID, &r.ID, &r.Name, &icon, &r.Mentionable, &r.Managed, &r.Hoist, &r.Color, &r.Position, &r.Permissions)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
r.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
r.Icon = icon.String
return r
return r, nil
}
func (r *Role) Upsert(txn dbutil.Execable) {

View file

@ -1,28 +1,36 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"errors"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type ThreadQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*Thread]
}
const (
threadSelect = "SELECT dcid, parent_chan_id, root_msg_dcid, root_msg_mxid, creation_notice_mxid FROM thread"
)
func (tq *ThreadQuery) New() *Thread {
return &Thread{
db: tq.db,
log: tq.log,
}
func newThread(qh *dbutil.QueryHelper[*Thread]) *Thread {
return &Thread{qh: qh}
}
func (tq *ThreadQuery) GetByDiscordID(discordID string) *Thread {
@ -59,8 +67,7 @@ func (tq *ThreadQuery) GetByMatrixRootOrCreationNoticeMsg(mxid id.EventID) *Thre
}
type Thread struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*Thread]
ID string
ParentID string
@ -71,16 +78,8 @@ type Thread struct {
CreationNoticeMXID id.EventID
}
func (t *Thread) Scan(row dbutil.Scannable) *Thread {
err := row.Scan(&t.ID, &t.ParentID, &t.RootDiscordID, &t.RootMXID, &t.CreationNoticeMXID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
t.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
}
return t
func (t *Thread) Scan(row dbutil.Scannable) (*Thread, error) {
return dbutil.ValueOrErr(t, row.Scan(&t.ID, &t.ParentID, &t.RootDiscordID, &t.RootMXID, &t.CreationNoticeMXID))
}
func (t *Thread) Insert() {

View file

@ -1,23 +1,34 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type UserQuery struct {
db *Database
log log.Logger
*dbutil.QueryHelper[*User]
}
func (uq *UserQuery) New() *User {
return &User{
db: uq.db,
log: uq.log,
}
func newUser(qh *dbutil.QueryHelper[*User]) *User {
return &User{qh: qh}
}
func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
@ -51,8 +62,7 @@ func (uq *UserQuery) GetAllWithToken() []*User {
}
type User struct {
db *Database
log log.Logger
qh *dbutil.QueryHelper[*User]
MXID id.UserID
DiscordID string
@ -64,22 +74,18 @@ type User struct {
ReadStateVersion int
}
func (u *User) Scan(row dbutil.Scannable) *User {
func (u *User) Scan(row dbutil.Scannable) (*User, error) {
var discordID, managementRoom, spaceRoom, dmSpaceRoom, discordToken sql.NullString
err := row.Scan(&u.MXID, &discordID, &discordToken, &managementRoom, &spaceRoom, &dmSpaceRoom, &u.ReadStateVersion)
if err != nil {
if err != sql.ErrNoRows {
u.log.Errorln("Database scan failed:", err)
panic(err)
}
return nil
return nil, err
}
u.DiscordID = discordID.String
u.DiscordToken = discordToken.String
u.ManagementRoom = id.RoomID(managementRoom.String)
u.SpaceRoom = id.RoomID(spaceRoom.String)
u.DMSpaceRoom = id.RoomID(dmSpaceRoom.String)
return u
return u, nil
}
func (u *User) Insert() {

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (

View file

@ -25,10 +25,8 @@ import (
"fmt"
"io"
"mime"
"mime/multipart"
"net"
"net/http"
"net/textproto"
"net/url"
"os"
"strconv"
@ -112,11 +110,9 @@ func newDirectMediaAPI(br *DiscordBridge) *DirectMediaAPI {
if dma.ks.WellKnownTarget == "" {
dma.ks.WellKnownTarget = fmt.Sprintf("%s:443", dma.cfg.ServerName)
}
federationRouter := r.PathPrefix("/_matrix/federation").Subrouter()
mediaRouter := r.PathPrefix("/_matrix/media").Subrouter()
clientMediaRouter := r.PathPrefix("/_matrix/client/v1/media").Subrouter()
var reqIDCounter atomic.Uint64
middleware := func(next http.Handler) http.Handler {
mediaRouter.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
@ -128,37 +124,21 @@ func newDirectMediaAPI(br *DiscordBridge) *DirectMediaAPI {
Logger()
next.ServeHTTP(w, r.WithContext(log.WithContext(r.Context())))
})
}
mediaRouter.Use(middleware)
federationRouter.Use(middleware)
clientMediaRouter.Use(middleware)
})
addRoutes := func(version string) {
mediaRouter.HandleFunc("/"+version+"/download/{serverName}/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet)
mediaRouter.HandleFunc("/"+version+"/download/{serverName}/{mediaID}/{fileName}", dma.DownloadMedia).Methods(http.MethodGet)
mediaRouter.HandleFunc("/"+version+"/thumbnail/{serverName}/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet)
mediaRouter.HandleFunc("/"+version+"/upload/{serverName}/{mediaID}", dma.UploadNotSupported).Methods(http.MethodPut)
mediaRouter.HandleFunc("/"+version+"/upload", dma.UploadNotSupported).Methods(http.MethodPost)
mediaRouter.HandleFunc("/"+version+"/create", dma.UploadNotSupported).Methods(http.MethodPost)
mediaRouter.HandleFunc("/"+version+"/config", dma.UploadNotSupported).Methods(http.MethodGet)
mediaRouter.HandleFunc("/"+version+"/preview_url", dma.PreviewURLNotSupported).Methods(http.MethodGet)
}
clientMediaRouter.HandleFunc("/download/{serverName}/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet)
clientMediaRouter.HandleFunc("/download/{serverName}/{mediaID}/{fileName}", dma.DownloadMedia).Methods(http.MethodGet)
clientMediaRouter.HandleFunc("/thumbnail/{serverName}/{mediaID}", dma.DownloadMedia).Methods(http.MethodGet)
clientMediaRouter.HandleFunc("/upload/{serverName}/{mediaID}", dma.UploadNotSupported).Methods(http.MethodPut)
clientMediaRouter.HandleFunc("/upload", dma.UploadNotSupported).Methods(http.MethodPost)
clientMediaRouter.HandleFunc("/create", dma.UploadNotSupported).Methods(http.MethodPost)
clientMediaRouter.HandleFunc("/config", dma.UploadNotSupported).Methods(http.MethodGet)
clientMediaRouter.HandleFunc("/preview_url", dma.PreviewURLNotSupported).Methods(http.MethodGet)
addRoutes("v3")
addRoutes("r0")
addRoutes("v1")
federationRouter.HandleFunc("/v1/media/download/{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)
federationRouter.NotFoundHandler = http.HandlerFunc(dma.UnknownEndpoint)
federationRouter.MethodNotAllowedHandler = http.HandlerFunc(dma.UnsupportedMethod)
dma.ks.Register(r)
return dma
@ -552,17 +532,14 @@ 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/")
vars := mux.Vars(r)
if !isNewFederation && vars["serverName"] != dma.cfg.ServerName {
if vars["serverName"] != dma.cfg.ServerName {
jsonResponse(w, http.StatusNotFound, &mautrix.RespError{
ErrCode: mautrix.MNotFound.ErrCode,
Err: fmt.Sprintf("This is a Discord media proxy for %q, other media downloads are not available here", dma.cfg.ServerName),
})
return
}
// TODO check destination header in X-Matrix auth when isNewFederation
url, expiresAt, err := dma.getMediaURL(ctx, vars["mediaID"])
if err != nil {
var respError *RespError
@ -579,36 +556,7 @@ func (dma *DirectMediaAPI) DownloadMedia(w http.ResponseWriter, r *http.Request)
})
}
return
}
if isNewFederation {
mp := multipart.NewWriter(w)
w.Header().Set("Content-Type", strings.Replace(mp.FormDataContentType(), "form-data", "mixed", 1))
var metaPart io.Writer
metaPart, err = mp.CreatePart(textproto.MIMEHeader{
"Content-Type": {"application/json"},
})
if err != nil {
log.Err(err).Msg("Failed to create multipart metadata field")
return
}
_, err = metaPart.Write([]byte(`{}`))
if err != nil {
log.Err(err).Msg("Failed to write multipart metadata field")
return
}
_, err = mp.CreatePart(textproto.MIMEHeader{
"Location": {url},
})
if err != nil {
log.Err(err).Msg("Failed to create multipart redirect field")
return
}
err = mp.Close()
if err != nil {
log.Err(err).Msg("Failed to close multipart writer")
return
}
return
}
// Proxy if the config allows proxying and the request doesn't allow redirects.
// In any other case, redirect to the Discord CDN.

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (

View file

@ -2,6 +2,9 @@
homeserver:
# The address that this appservice can use to connect to the homeserver.
address: https://matrix.example.com
# Publicly accessible base URL for media, used for avatars in relay mode.
# If not set, the connection address above will be used.
public_address: null
# The domain of the homeserver (also known as server_name, used for MXIDs, etc).
domain: example.com
@ -110,13 +113,6 @@ bridge:
# If set to `never`, DM rooms will never have names and avatars set.
private_chat_portal_meta: default
# Publicly accessible base URL that Discord can use to reach the bridge, used for avatars in relay mode.
# If not set, avatars will not be bridged. Only the /mautrix-discord/avatar/{server}/{id}/{hash} endpoint is used on this address.
# This should not have a trailing slash, the endpoint above will be appended to the provided address.
public_address: null
# A random key used to sign the avatar URLs. The bridge will only accept requests with a valid signature.
avatar_proxy_key: generate
portal_message_buffer: 128
# Number of private channel portals to create on bridge startup.

View file

@ -30,7 +30,6 @@ import (
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database"
)
@ -263,19 +262,11 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
}
switch node := n.(type) {
case *astDiscordUserMention:
var mxid id.UserID
var name string
if puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10)); puppet != nil {
mxid = puppet.MXID
name = puppet.Name
}
if user := node.portal.bridge.GetUserByID(strconv.FormatInt(node.id, 10)); user != nil {
mxid = user.MXID
if name == "" {
name = user.MXID.Localpart()
}
_, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, user.MXID.URI().MatrixToURL(), user.MXID)
} else if puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10)); puppet != nil {
_, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, puppet.MXID.URI().MatrixToURL(), puppet.Name)
}
_, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, mxid.URI().MatrixToURL(), name)
return
case *astDiscordRoleMention:
role := node.portal.bridge.DB.Role.GetByID(node.portal.GuildID, strconv.FormatInt(node.id, 10))

24
go.mod
View file

@ -10,15 +10,15 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.22
github.com/rs/zerolog v1.31.0
github.com/rs/zerolog v1.32.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.4
github.com/yuin/goldmark v1.6.0
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
golang.org/x/sync v0.5.0
github.com/stretchr/testify v1.9.0
github.com/yuin/goldmark v1.7.0
go.mau.fi/util v0.4.1-0.20240311141448-53cb04950f7e
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
golang.org/x/sync v0.6.0
maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c
maunium.net/go/mautrix v0.18.0-beta.1.0.20240312191539-8128b00e0082
)
require (
@ -27,17 +27,17 @@ require (
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
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/zeroconfig v0.1.2 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect
)
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20231013182643-f333f2578a3c
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20240312193245-78a9f3937dd2

52
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-20231013182643-f333f2578a3c h1:WaJ9eX8eyOBHD8te5t7xzm27uwhfaN94o8vUVFXliyA=
github.com/beeper/discordgo v0.0.0-20231013182643-f333f2578a3c/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/beeper/discordgo v0.0.0-20240312193245-78a9f3937dd2 h1:VEuPDZd9xUL+yNkqG4WnQrqKRiVdpXpK2wLPo07CKM8=
github.com/beeper/discordgo v0.0.0-20240312193245-78a9f3937dd2/go.mod h1:59+AOzzjmL6onAh62nuLXmn7dJCaC/owDLWbGtjTcFA=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -28,40 +28,40 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.mau.fi/util v0.4.1-0.20240311141448-53cb04950f7e h1:e1jDj/MjleSS5r9DMRbuCZYKy5Rr+sbsu8eWjtLqrGk=
go.mau.fi/util v0.4.1-0.20240311141448-53cb04950f7e/go.mod h1:jOAREC/go8T6rGic01cu6WRa90xi9U4z3QmDjRf8xpo=
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@ -72,5 +72,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/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.20240712164054-e6046fbf432c h1:LHjqti3fFzrC8LXkkxxKYlLbuI/CJcwa2JN4Ppg2GK0=
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c/go.mod h1:gCgLw/4c1a8QsiOWTdUdXlt5cYdE0rJ9wLeZQKPD58Q=
maunium.net/go/mautrix v0.18.0-beta.1.0.20240312191539-8128b00e0082 h1:63D5huHDTcOCQ69HVkyuaKlp2/9fFmQcnGpiFddTq9c=
maunium.net/go/mautrix v0.18.0-beta.1.0.20240312191539-8128b00e0082/go.mod h1:0sfLB2ejW+lhgio4UlZMmn5i9SuZ8mxFkonFSamrfTE=

View file

@ -18,7 +18,6 @@ package main
import (
_ "embed"
"net/http"
"sync"
"go.mau.fi/util/configupgrade"
@ -106,9 +105,6 @@ func (br *DiscordBridge) Start() {
if br.Config.Bridge.Provisioning.SharedSecret != "disable" {
br.provisioning = newProvisioningAPI(br)
}
if br.Config.Bridge.PublicAddress != "" {
br.AS.Router.HandleFunc("/mautrix-discord/avatar/{server}/{mediaID}/{checksum}", br.serveMediaProxy).Methods(http.MethodGet)
}
br.DMA = newDirectMediaAPI(br)
br.WaitWebsocketConnected()
go br.startUsers()
@ -185,7 +181,7 @@ func main() {
Name: "mautrix-discord",
URL: "https://github.com/mautrix/discord",
Description: "A Matrix-Discord puppeting bridge.",
Version: "0.7.0",
Version: "0.6.5",
ProtocolName: "Discord",
BeeperServiceName: "discordgo",
BeeperNetworkName: "discord",

102
portal.go
View file

@ -1,15 +1,27 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"regexp"
@ -21,7 +33,6 @@ import (
"github.com/bwmarrin/discordgo"
"github.com/gabriel-vasile/mimetype"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/util/variationselector"
@ -464,7 +475,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, channel *discordgo.Channel) e
Content: event.Content{Parsed: &event.JoinRulesEventContent{
JoinRule: event.JoinRuleRestricted,
Allow: []event.JoinRuleAllow{{
RoomID: portal.Guild.MXID,
RoomID: spaceID,
Type: event.JoinRuleAllowRoomMembership,
}},
}},
@ -524,7 +535,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, channel *discordgo.Channel) e
if portal.GuildID == "" {
user.addPrivateChannelToSpace(portal)
} else {
portal.updateSpace(user)
portal.updateSpace()
}
portal.ensureUserInvited(user, true)
user.syncChatDoublePuppetDetails(portal, true)
@ -1375,64 +1386,6 @@ func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part strin
}
}
func (br *DiscordBridge) serveMediaProxy(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
mxc := id.ContentURI{
Homeserver: vars["server"],
FileID: vars["mediaID"],
}
checksum, err := base64.RawURLEncoding.DecodeString(vars["checksum"])
if err != nil || len(checksum) != 32 {
w.WriteHeader(http.StatusNotFound)
return
}
_, expectedChecksum := br.hashMediaProxyURL(mxc)
if !hmac.Equal(checksum, expectedChecksum) {
w.WriteHeader(http.StatusNotFound)
return
}
reader, err := br.Bot.Download(mxc)
if err != nil {
br.ZLog.Warn().Err(err).Msg("Failed to download media to proxy")
w.WriteHeader(http.StatusInternalServerError)
return
}
buf := make([]byte, 32*1024)
n, err := io.ReadFull(reader, buf)
if err != nil && (!errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF)) {
br.ZLog.Warn().Err(err).Msg("Failed to read first part of media to proxy")
w.WriteHeader(http.StatusBadGateway)
return
}
w.Header().Add("Content-Type", http.DetectContentType(buf[:n]))
if n < len(buf) {
w.Header().Add("Content-Length", strconv.Itoa(n))
}
w.WriteHeader(http.StatusOK)
_, err = w.Write(buf[:n])
if err != nil {
return
}
if n >= len(buf) {
_, _ = io.CopyBuffer(w, reader, buf)
}
}
func (br *DiscordBridge) hashMediaProxyURL(mxc id.ContentURI) (string, []byte) {
path := fmt.Sprintf("/mautrix-discord/avatar/%s/%s/", mxc.Homeserver, mxc.FileID)
checksum := hmac.New(sha256.New, []byte(br.Config.Bridge.AvatarProxyKey))
checksum.Write([]byte(path))
return path, checksum.Sum(nil)
}
func (br *DiscordBridge) makeMediaProxyURL(mxc id.ContentURI) string {
if br.Config.Bridge.PublicAddress == "" {
return ""
}
path, checksum := br.hashMediaProxyURL(mxc)
return br.Config.Bridge.PublicAddress + path + base64.RawURLEncoding.EncodeToString(checksum)
}
func (portal *Portal) getRelayUserMeta(sender *User) (name, avatarURL string) {
member := portal.bridge.StateStore.GetMember(portal.MXID, sender.MXID)
name = member.Displayname
@ -1440,8 +1393,11 @@ func (portal *Portal) getRelayUserMeta(sender *User) (name, avatarURL string) {
name = sender.MXID.String()
}
mxc := member.AvatarURL.ParseOrIgnore()
if !mxc.IsEmpty() && portal.bridge.Config.Bridge.PublicAddress != "" {
avatarURL = portal.bridge.makeMediaProxyURL(mxc)
if !mxc.IsEmpty() {
avatarURL = mautrix.BuildURL(
portal.bridge.PublicHSAddress,
"_matrix", "media", "v3", "download", mxc.Homeserver, mxc.FileID,
).String()
}
return
}
@ -2381,19 +2337,11 @@ func (portal *Portal) ExpectedSpaceID() id.RoomID {
return ""
}
func (portal *Portal) updateSpace(source *User) bool {
func (portal *Portal) updateSpace() bool {
if portal.MXID == "" {
return false
}
if portal.Parent != nil {
if portal.Parent.MXID != "" {
portal.log.Warn().Str("parent_id", portal.ParentID).Msg("Parent portal has no Matrix room, creating...")
err := portal.Parent.CreateMatrixRoom(source, nil)
if err != nil {
portal.log.Err(err).Str("parent_id", portal.ParentID).Msg("Failed to create Matrix room for parent")
return false
}
}
return portal.addToSpace(portal.Parent.MXID)
} else if portal.Guild != nil {
return portal.addToSpace(portal.Guild.MXID)
@ -2484,7 +2432,7 @@ func (portal *Portal) UpdateInfo(source *User, meta *discordgo.Channel) *discord
changed = portal.UpdateParent(meta.ParentID) || changed
// Private channels are added to the space in User.handlePrivateChannel
if portal.GuildID != "" && portal.MXID != "" && portal.ExpectedSpaceID() != portal.InSpace {
changed = portal.updateSpace(source) || changed
changed = portal.updateSpace() || changed
}
if changed {
portal.UpdateBridgeInfo()

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (

View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (

16
user.go
View file

@ -1,3 +1,19 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (