Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69f1793e24 | ||
|
|
eab19f6679 | ||
|
|
839933005c | ||
|
|
a28735beb7 | ||
|
|
5d7a6e7088 | ||
|
|
f9ba906bbd | ||
|
|
41d51ec992 | ||
|
|
6ccf87bc0a | ||
|
|
011c60610a | ||
|
|
669964272e | ||
|
|
943f2dd6f0 | ||
|
|
3e5baa502e | ||
|
|
c336804c7e | ||
|
|
2421cd7817 | ||
|
|
a7864c28d8 | ||
|
|
0dba4fbdd4 | ||
|
|
fac7d79c5e | ||
|
|
f32fd8d904 | ||
|
|
1e81fc6a02 | ||
|
|
80f8bed9b9 | ||
|
|
7cdd1bb9e4 | ||
|
|
a2121347e8 | ||
|
|
85395c0230 | ||
|
|
787ce75dde | ||
|
|
5b715cd9e2 | ||
|
|
a9e03f092c | ||
|
|
466139164c | ||
|
|
e183f5cffa | ||
|
|
e7615ef4be | ||
|
|
694733a4e9 | ||
|
|
6f4c51852c |
8
.github/workflows/go.yml
vendored
8
.github/workflows/go.yml
vendored
@@ -5,17 +5,13 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.19]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Install libolm
|
||||
run: sudo apt-get install libolm-dev libolm3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude_types: [markdown]
|
||||
@@ -9,7 +9,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||
rev: v1.0.0-beta.5
|
||||
rev: v1.0.0-rc.1
|
||||
hooks:
|
||||
- id: go-imports-repo
|
||||
- id: go-vet-repo-mod
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
# v0.1.1 (2023-02-16)
|
||||
|
||||
* Started automatically subscribing to bridged guilds. This fixes two problems:
|
||||
* Typing notifications should now work automatically in guilds.
|
||||
* Huge guilds now actually get messages bridged.
|
||||
* Added support for converting animated lottie stickers to raster formats using
|
||||
[lottieconverter](https://github.com/sot-tech/LottieConverter).
|
||||
* Added basic bridging for call start and guild join messages.
|
||||
* Improved markdown parsing to disable more features that don't exist on Discord.
|
||||
* Removed width from inline images (e.g. in the `guilds status` output) to
|
||||
handle non-square images properly.
|
||||
* Fixed ghost user info not being synced when receiving reactions.
|
||||
|
||||
# v0.1.0 (2023-01-29)
|
||||
|
||||
Initial release.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie
|
||||
|
||||
FROM golang:1-alpine3.17 AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
||||
@@ -11,8 +13,11 @@ FROM alpine:3.17
|
||||
ENV UID=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 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 --from=builder /usr/bin/mautrix-discord /usr/bin/mautrix-discord
|
||||
COPY --from=builder /build/example-config.yaml /opt/mautrix-discord/example-config.yaml
|
||||
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie
|
||||
|
||||
FROM alpine:3.17
|
||||
|
||||
ENV UID=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 \
|
||||
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
|
||||
COPY $EXECUTABLE /usr/bin/mautrix-discord
|
||||
COPY ./example-config.yaml /opt/mautrix-discord/example-config.yaml
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie
|
||||
|
||||
FROM golang:1-alpine3.17 AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl
|
||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl \
|
||||
zlib libpng giflib libstdc++ libgcc
|
||||
|
||||
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
|
||||
|
||||
184
attachments.go
184
attachments.go
@@ -2,10 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -16,6 +21,9 @@ import (
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/crypto/attachment"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/util"
|
||||
"maunium.net/go/mautrix/util/ffmpeg"
|
||||
|
||||
"go.mau.fi/mautrix-discord/database"
|
||||
)
|
||||
@@ -62,7 +70,7 @@ func uploadDiscordAttachment(url string, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventContent) ([]byte, error) {
|
||||
func downloadMatrixAttachment(intent *appservice.IntentAPI, content *event.MessageEventContent) ([]byte, error) {
|
||||
var file *event.EncryptedFileInfo
|
||||
rawMXC := content.URL
|
||||
|
||||
@@ -76,7 +84,7 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := portal.MainIntent().DownloadBytes(mxc)
|
||||
data, err := intent.DownloadBytes(mxc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,23 +99,24 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, attachmentID, mime string) (*database.File, error) {
|
||||
func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, meta AttachmentMeta) (*database.File, error) {
|
||||
dbFile := br.DB.File.New()
|
||||
dbFile.Timestamp = time.Now()
|
||||
dbFile.URL = url
|
||||
dbFile.ID = attachmentID
|
||||
dbFile.ID = meta.AttachmentID
|
||||
dbFile.EmojiName = meta.EmojiName
|
||||
dbFile.Size = len(data)
|
||||
dbFile.MimeType = mimetype.Detect(data).String()
|
||||
if mime == "" {
|
||||
mime = dbFile.MimeType
|
||||
if meta.MimeType == "" {
|
||||
meta.MimeType = dbFile.MimeType
|
||||
}
|
||||
if strings.HasPrefix(mime, "image/") {
|
||||
if strings.HasPrefix(meta.MimeType, "image/") {
|
||||
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
||||
dbFile.Width = cfg.Width
|
||||
dbFile.Height = cfg.Height
|
||||
}
|
||||
|
||||
uploadMime := mime
|
||||
uploadMime := meta.MimeType
|
||||
if encrypt {
|
||||
dbFile.Encrypted = true
|
||||
dbFile.DecryptionInfo = attachment.NewEncryptedFile()
|
||||
@@ -140,22 +149,161 @@ func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, da
|
||||
}
|
||||
dbFile.MXC = uploaded.ContentURI
|
||||
}
|
||||
dbFile.Insert(nil)
|
||||
return dbFile, nil
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, attachmentID, mime string) (*database.File, error) {
|
||||
dbFile := br.DB.File.Get(url, encrypt)
|
||||
if dbFile == nil {
|
||||
data, err := downloadDiscordAttachment(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
type AttachmentMeta struct {
|
||||
AttachmentID string
|
||||
MimeType string
|
||||
EmojiName string
|
||||
CopyIfMissing bool
|
||||
Converter func([]byte) ([]byte, string, error)
|
||||
}
|
||||
|
||||
var NoMeta = AttachmentMeta{}
|
||||
|
||||
type attachmentKey struct {
|
||||
URL string
|
||||
Encrypt bool
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) convertLottie(data []byte) ([]byte, string, error) {
|
||||
fps := br.Config.Bridge.AnimatedSticker.Args.FPS
|
||||
width := br.Config.Bridge.AnimatedSticker.Args.Width
|
||||
height := br.Config.Bridge.AnimatedSticker.Args.Height
|
||||
target := br.Config.Bridge.AnimatedSticker.Target
|
||||
var lottieTarget, outputMime string
|
||||
switch target {
|
||||
case "png":
|
||||
lottieTarget = "png"
|
||||
outputMime = "image/png"
|
||||
fps = 1
|
||||
case "gif":
|
||||
lottieTarget = "gif"
|
||||
outputMime = "image/gif"
|
||||
case "webm":
|
||||
lottieTarget = "pngs"
|
||||
outputMime = "video/webm"
|
||||
case "webp":
|
||||
lottieTarget = "pngs"
|
||||
outputMime = "image/webp"
|
||||
case "disable":
|
||||
return data, "application/json", nil
|
||||
default:
|
||||
return nil, "", fmt.Errorf("invalid animated sticker target %q in bridge config", br.Config.Bridge.AnimatedSticker.Target)
|
||||
}
|
||||
|
||||
dbFile, err = br.uploadMatrixAttachment(intent, data, url, encrypt, attachmentID, mime)
|
||||
ctx := context.Background()
|
||||
tempdir, err := os.MkdirTemp("", "mautrix_discord_lottie_")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
removErr := os.RemoveAll(tempdir)
|
||||
if removErr != nil {
|
||||
br.Log.Warnfln("Failed to delete lottie conversion temp dir: %v", removErr)
|
||||
}
|
||||
return dbFile, nil
|
||||
}()
|
||||
|
||||
lottieOutput := filepath.Join(tempdir, "out_")
|
||||
if lottieTarget != "pngs" {
|
||||
lottieOutput = filepath.Join(tempdir, "output."+lottieTarget)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "lottieconverter", "-", lottieOutput, lottieTarget, fmt.Sprintf("%dx%d", width, height), strconv.Itoa(fps))
|
||||
cmd.Stdin = bytes.NewReader(data)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to run lottieconverter: %w", err)
|
||||
}
|
||||
var path string
|
||||
if lottieTarget == "pngs" {
|
||||
var videoCodec string
|
||||
outputExtension := "." + target
|
||||
if target == "webm" {
|
||||
videoCodec = "libvpx-vp9"
|
||||
} else if target == "webp" {
|
||||
videoCodec = "libwebp_anim"
|
||||
} else {
|
||||
panic(fmt.Errorf("impossible case: unknown target %q", target))
|
||||
}
|
||||
path, err = ffmpeg.ConvertPath(
|
||||
ctx, lottieOutput+"*.png", outputExtension,
|
||||
[]string{"-framerate", strconv.Itoa(fps), "-pattern_type", "glob"},
|
||||
[]string{"-c:v", videoCodec, "-pix_fmt", "yuva420p", "-f", target},
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to run ffmpeg: %w", err)
|
||||
}
|
||||
} else {
|
||||
path = lottieOutput
|
||||
}
|
||||
data, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to read converted file: %w", err)
|
||||
}
|
||||
return data, outputMime, nil
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta AttachmentMeta) (returnDBFile *database.File, returnErr error) {
|
||||
isCacheable := !encrypt
|
||||
returnDBFile = br.DB.File.Get(url, encrypt)
|
||||
if returnDBFile == nil {
|
||||
transferKey := attachmentKey{url, encrypt}
|
||||
once, _ := br.attachmentTransfers.GetOrSet(transferKey, &util.ReturnableOnce[*database.File]{})
|
||||
returnDBFile, returnErr = once.Do(func() (onceDBFile *database.File, onceErr error) {
|
||||
if isCacheable {
|
||||
onceDBFile = br.DB.File.Get(url, encrypt)
|
||||
if onceDBFile != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var data []byte
|
||||
data, onceErr = downloadDiscordAttachment(url)
|
||||
if onceErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if meta.Converter != nil {
|
||||
data, meta.MimeType, onceErr = meta.Converter(data)
|
||||
if onceErr != nil {
|
||||
onceErr = fmt.Errorf("failed to convert attachment: %w", onceErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onceDBFile, onceErr = br.uploadMatrixAttachment(intent, data, url, encrypt, meta)
|
||||
if onceErr != nil {
|
||||
return
|
||||
}
|
||||
if isCacheable {
|
||||
onceDBFile.Insert(nil)
|
||||
}
|
||||
br.attachmentTransfers.Delete(transferKey)
|
||||
return
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
|
||||
var url, mimeType string
|
||||
if animated {
|
||||
url = discordgo.EndpointEmojiAnimated(emojiID)
|
||||
mimeType = "image/gif"
|
||||
} else {
|
||||
url = discordgo.EndpointEmoji(emojiID)
|
||||
mimeType = "image/png"
|
||||
}
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, AttachmentMeta{
|
||||
AttachmentID: emojiID,
|
||||
MimeType: mimeType,
|
||||
EmojiName: name,
|
||||
})
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to download emoji %s from discord: %v", emojiID, err)
|
||||
return id.ContentURI{}
|
||||
}
|
||||
return dbFile.MXC
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ func fnListGuilds(ce *WrappedCommandEvent) {
|
||||
}
|
||||
var avatarHTML string
|
||||
if !guild.AvatarURL.IsEmpty() {
|
||||
avatarHTML = fmt.Sprintf(`<img data-mx-emoticon height="24" width="24" src="%s" alt="" title="Guild avatar"> `, guild.AvatarURL.String())
|
||||
avatarHTML = fmt.Sprintf(`<img data-mx-emoticon height="24" src="%s" alt="" title="Guild avatar"> `, guild.AvatarURL.String())
|
||||
}
|
||||
items = append(items, fmt.Sprintf("<li>%s%s (<code>%s</code>) - %s</li>", avatarHTML, html.EscapeString(guild.Name), guild.ID, status))
|
||||
}
|
||||
|
||||
@@ -50,6 +50,14 @@ type BridgeConfig struct {
|
||||
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
|
||||
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
|
||||
FederateRooms bool `yaml:"federate_rooms"`
|
||||
AnimatedSticker struct {
|
||||
Target string `yaml:"target"`
|
||||
Args struct {
|
||||
Width int `yaml:"width"`
|
||||
Height int `yaml:"height"`
|
||||
FPS int `yaml:"fps"`
|
||||
} `yaml:"args"`
|
||||
} `yaml:"animated_sticker"`
|
||||
|
||||
DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"`
|
||||
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||
|
||||
@@ -45,6 +45,10 @@ func DoUpgrade(helper *up.Helper) {
|
||||
helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete")
|
||||
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
|
||||
helper.Copy(up.Bool, "bridge", "federate_rooms")
|
||||
helper.Copy(up.Str, "bridge", "animated_sticker", "target")
|
||||
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "width")
|
||||
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "height")
|
||||
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "fps")
|
||||
helper.Copy(up.Map, "bridge", "double_puppet_server_map")
|
||||
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
||||
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
||||
|
||||
@@ -21,7 +21,6 @@ type Database struct {
|
||||
Message *MessageQuery
|
||||
Thread *ThreadQuery
|
||||
Reaction *ReactionQuery
|
||||
Emoji *EmojiQuery
|
||||
Guild *GuildQuery
|
||||
Role *RoleQuery
|
||||
File *FileQuery
|
||||
@@ -54,10 +53,6 @@ func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
|
||||
db: db,
|
||||
log: log.Sub("Reaction"),
|
||||
}
|
||||
db.Emoji = &EmojiQuery{
|
||||
db: db,
|
||||
log: log.Sub("Emoji"),
|
||||
}
|
||||
db.Guild = &GuildQuery{
|
||||
db: db,
|
||||
log: log.Sub("Guild"),
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/util/dbutil"
|
||||
)
|
||||
|
||||
type EmojiQuery struct {
|
||||
db *Database
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
emojiSelect = "SELECT discord_id, discord_name, matrix_url FROM emoji"
|
||||
)
|
||||
|
||||
func (eq *EmojiQuery) New() *Emoji {
|
||||
return &Emoji{
|
||||
db: eq.db,
|
||||
log: eq.log,
|
||||
}
|
||||
}
|
||||
|
||||
func (eq *EmojiQuery) GetByDiscordID(discordID string) *Emoji {
|
||||
query := emojiSelect + " WHERE discord_id=$1"
|
||||
return eq.get(query, discordID)
|
||||
}
|
||||
|
||||
func (eq *EmojiQuery) GetByMatrixURL(matrixURL id.ContentURI) *Emoji {
|
||||
query := emojiSelect + " WHERE matrix_url=$1"
|
||||
return eq.get(query, matrixURL.String())
|
||||
}
|
||||
|
||||
func (eq *EmojiQuery) get(query string, args ...interface{}) *Emoji {
|
||||
return eq.New().Scan(eq.db.QueryRow(query, args...))
|
||||
}
|
||||
|
||||
type Emoji struct {
|
||||
db *Database
|
||||
log log.Logger
|
||||
|
||||
DiscordID string
|
||||
DiscordName string
|
||||
|
||||
MatrixURL id.ContentURI
|
||||
}
|
||||
|
||||
func (e *Emoji) Scan(row dbutil.Scannable) *Emoji {
|
||||
var matrixURL sql.NullString
|
||||
|
||||
err := row.Scan(&e.DiscordID, &e.DiscordName, &matrixURL)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
e.log.Errorln("Database scan failed:", err)
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
e.MatrixURL, _ = id.ParseContentURI(matrixURL.String)
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Emoji) Insert() {
|
||||
query := "INSERT INTO emoji" +
|
||||
" (discord_id, discord_name, matrix_url)" +
|
||||
" VALUES ($1, $2, $3);"
|
||||
|
||||
_, err := e.db.Exec(query, e.DiscordID, e.DiscordName, e.MatrixURL.String())
|
||||
|
||||
if err != nil {
|
||||
e.log.Warnfln("Failed to insert emoji %s: %v", e.DiscordID, err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Emoji) Delete() {
|
||||
query := "DELETE FROM emoji WHERE discord_id=$1"
|
||||
|
||||
_, err := e.db.Exec(query, e.DiscordID)
|
||||
if err != nil {
|
||||
e.log.Warnfln("Failed to delete emoji %s: %v", e.DiscordID, err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Emoji) APIName() string {
|
||||
if e.DiscordID != "" && e.DiscordName != "" {
|
||||
return e.DiscordName + ":" + e.DiscordID
|
||||
} else if e.DiscordName != "" {
|
||||
return e.DiscordName
|
||||
}
|
||||
return e.DiscordID
|
||||
}
|
||||
@@ -20,10 +20,10 @@ type FileQuery struct {
|
||||
|
||||
// language=postgresql
|
||||
const (
|
||||
fileSelect = "SELECT url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp FROM discord_file"
|
||||
fileSelect = "SELECT url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp FROM discord_file"
|
||||
fileInsert = `
|
||||
INSERT INTO discord_file (url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
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)
|
||||
`
|
||||
)
|
||||
|
||||
@@ -39,15 +39,21 @@ func (fq *FileQuery) Get(url string, encrypted bool) *File {
|
||||
return fq.New().Scan(fq.db.QueryRow(query, url, encrypted))
|
||||
}
|
||||
|
||||
func (fq *FileQuery) GetByMXC(mxc id.ContentURI) *File {
|
||||
query := fileSelect + " WHERE mxc=$1"
|
||||
return fq.New().Scan(fq.db.QueryRow(query, mxc.String()))
|
||||
}
|
||||
|
||||
type File struct {
|
||||
db *Database
|
||||
log log.Logger
|
||||
|
||||
URL string
|
||||
Encrypted bool
|
||||
MXC id.ContentURI
|
||||
|
||||
ID string
|
||||
MXC id.ContentURI
|
||||
EmojiName string
|
||||
|
||||
Size int
|
||||
Width int
|
||||
@@ -55,16 +61,15 @@ type File struct {
|
||||
MimeType string
|
||||
|
||||
DecryptionInfo *attachment.EncryptedFile
|
||||
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (f *File) Scan(row dbutil.Scannable) *File {
|
||||
var fileID, decryptionInfo sql.NullString
|
||||
var fileID, emojiName, decryptionInfo sql.NullString
|
||||
var width, height sql.NullInt32
|
||||
var timestamp int64
|
||||
var mxc string
|
||||
err := row.Scan(&f.URL, &f.Encrypted, &fileID, &mxc, &f.Size, &width, &height, &f.MimeType, &decryptionInfo, ×tamp)
|
||||
err := row.Scan(&f.URL, &f.Encrypted, &mxc, &fileID, &emojiName, &f.Size, &width, &height, &f.MimeType, &decryptionInfo, ×tamp)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
f.log.Errorln("Database scan failed:", err)
|
||||
@@ -73,6 +78,7 @@ func (f *File) Scan(row dbutil.Scannable) *File {
|
||||
return nil
|
||||
}
|
||||
f.ID = fileID.String
|
||||
f.EmojiName = emojiName.String
|
||||
f.Timestamp = time.UnixMilli(timestamp)
|
||||
f.Width = int(width.Int32)
|
||||
f.Height = int(height.Int32)
|
||||
@@ -114,7 +120,7 @@ func (f *File) Insert(txn dbutil.Execable) {
|
||||
decryptionInfoStr.String = string(decryptionInfo)
|
||||
}
|
||||
_, err := txn.Exec(fileInsert,
|
||||
f.URL, f.Encrypted, strPtr(f.ID), f.MXC.String(), f.Size,
|
||||
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(),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- v0 -> v12: Latest revision
|
||||
-- v0 -> v13: Latest revision
|
||||
|
||||
CREATE TABLE guild (
|
||||
dcid TEXT PRIMARY KEY,
|
||||
@@ -126,12 +126,6 @@ CREATE TABLE reaction (
|
||||
CONSTRAINT reaction_message_fkey FOREIGN KEY (dc_msg_id, dc_first_attachment_id, _dc_first_edit_index, dc_chan_id, dc_chan_receiver) REFERENCES message (dcid, dc_attachment_id, dc_edit_index, dc_chan_id, dc_chan_receiver) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE emoji (
|
||||
discord_id TEXT PRIMARY KEY,
|
||||
discord_name TEXT,
|
||||
matrix_url TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE role (
|
||||
dc_guild_id TEXT,
|
||||
dcid TEXT,
|
||||
@@ -154,17 +148,16 @@ CREATE TABLE role (
|
||||
CREATE TABLE discord_file (
|
||||
url TEXT,
|
||||
encrypted BOOLEAN,
|
||||
mxc TEXT NOT NULL UNIQUE,
|
||||
|
||||
id TEXT,
|
||||
mxc TEXT NOT NULL,
|
||||
emoji_name TEXT,
|
||||
|
||||
size BIGINT NOT NULL,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
mime_type TEXT NOT NULL,
|
||||
|
||||
decryption_info jsonb,
|
||||
|
||||
timestamp BIGINT NOT NULL,
|
||||
|
||||
PRIMARY KEY (url, encrypted)
|
||||
|
||||
4
database/upgrades/13-merge-emoji-and-file.postgres.sql
Normal file
4
database/upgrades/13-merge-emoji-and-file.postgres.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- v13: Merge tables used for cached custom emojis and attachments
|
||||
ALTER TABLE discord_file ADD CONSTRAINT mxc_unique UNIQUE (mxc);
|
||||
ALTER TABLE discord_file ADD COLUMN emoji_name TEXT;
|
||||
DROP TABLE emoji;
|
||||
24
database/upgrades/13-merge-emoji-and-file.sqlite.sql
Normal file
24
database/upgrades/13-merge-emoji-and-file.sqlite.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- v13: Merge tables used for cached custom emojis and attachments
|
||||
CREATE TABLE new_discord_file (
|
||||
url TEXT,
|
||||
encrypted BOOLEAN,
|
||||
mxc TEXT NOT NULL UNIQUE,
|
||||
|
||||
id TEXT,
|
||||
emoji_name TEXT,
|
||||
|
||||
size BIGINT NOT NULL,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
mime_type TEXT NOT NULL,
|
||||
decryption_info jsonb,
|
||||
timestamp BIGINT NOT NULL,
|
||||
|
||||
PRIMARY KEY (url, encrypted)
|
||||
);
|
||||
|
||||
INSERT INTO new_discord_file (url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp)
|
||||
SELECT url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp FROM discord_file;
|
||||
|
||||
DROP TABLE discord_file;
|
||||
ALTER TABLE new_discord_file RENAME TO discord_file;
|
||||
79
emoji.go
79
emoji.go
@@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
|
||||
dbEmoji := portal.bridge.DB.Emoji.GetByDiscordID(emojiID)
|
||||
|
||||
if dbEmoji == nil {
|
||||
data, mimeType, err := portal.downloadDiscordEmoji(emojiID, animated)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to download emoji %s from discord: %v", emojiID, err)
|
||||
return id.ContentURI{}
|
||||
}
|
||||
|
||||
uri, err := portal.uploadMatrixEmoji(portal.MainIntent(), data, mimeType)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to upload discord emoji %s to homeserver: %v", emojiID, err)
|
||||
return id.ContentURI{}
|
||||
}
|
||||
|
||||
dbEmoji = portal.bridge.DB.Emoji.New()
|
||||
dbEmoji.DiscordID = emojiID
|
||||
dbEmoji.DiscordName = name
|
||||
dbEmoji.MatrixURL = uri
|
||||
dbEmoji.Insert()
|
||||
}
|
||||
|
||||
return dbEmoji.MatrixURL
|
||||
}
|
||||
|
||||
func (portal *Portal) downloadDiscordEmoji(id string, animated bool) ([]byte, string, error) {
|
||||
var url string
|
||||
var mimeType string
|
||||
|
||||
if animated {
|
||||
// This url requests a gif, so that's what we set the mimetype to.
|
||||
url = discordgo.EndpointEmojiAnimated(id)
|
||||
mimeType = "image/gif"
|
||||
} else {
|
||||
// This url requests a png, so that's what we set the mimetype to.
|
||||
url = discordgo.EndpointEmoji(id)
|
||||
mimeType = "image/png"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, mimeType, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", discordgo.DroidBrowserUserAgent)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, mimeType, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return data, mimeType, err
|
||||
}
|
||||
|
||||
func (portal *Portal) uploadMatrixEmoji(intent *appservice.IntentAPI, data []byte, mimeType string) (id.ContentURI, error) {
|
||||
uploaded, err := intent.UploadBytes(data, mimeType)
|
||||
if err != nil {
|
||||
return id.ContentURI{}, err
|
||||
}
|
||||
|
||||
return uploaded.ContentURI, nil
|
||||
}
|
||||
@@ -140,6 +140,20 @@ bridge:
|
||||
# Whether or not created rooms should have federation enabled.
|
||||
# If false, created portal rooms will never be federated.
|
||||
federate_rooms: true
|
||||
# Settings for converting animated stickers.
|
||||
animated_sticker:
|
||||
# Format to which animated stickers should be converted.
|
||||
# disable - No conversion, send as-is (lottie JSON)
|
||||
# png - converts to non-animated png (fastest)
|
||||
# gif - converts to animated gif
|
||||
# webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support
|
||||
# webp - converts to animated webp, requires ffmpeg executable with webp codec/container support
|
||||
target: webp
|
||||
# Arguments for converter. All converters take width and height.
|
||||
args:
|
||||
width: 320
|
||||
height: 320
|
||||
fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)
|
||||
# Servers to always allow double puppeting from
|
||||
double_puppet_server_map:
|
||||
example.com: https://example.com
|
||||
|
||||
59
formatter.go
59
formatter.go
@@ -1,5 +1,5 @@
|
||||
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||
// Copyright (C) 2022 Tulir Asokan
|
||||
// Copyright (C) 2023 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
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
@@ -31,24 +33,57 @@ import (
|
||||
"maunium.net/go/mautrix/util/variationselector"
|
||||
)
|
||||
|
||||
var discordExtensions = goldmark.WithExtensions(mdext.SimpleSpoiler, mdext.DiscordUnderline, &DiscordEveryone{})
|
||||
// escapeFixer is a hacky partial fix for the difference in escaping markdown, used with escapeReplacement
|
||||
//
|
||||
// Discord allows escaping with just one backslash, e.g. \__a__,
|
||||
// but standard markdown requires both to be escaped (\_\_a__)
|
||||
var escapeFixer = regexp.MustCompile(`\\(__[^_]|\*\*[^*])`)
|
||||
|
||||
func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string) string {
|
||||
text = escapeFixer.ReplaceAllStringFunc(text, func(s string) string {
|
||||
func escapeReplacement(s string) string {
|
||||
return s[:2] + `\` + s[2:]
|
||||
})
|
||||
}
|
||||
|
||||
mdRenderer := goldmark.New(
|
||||
goldmark.WithParser(mdext.ParserWithoutFeatures(
|
||||
// indentableParagraphParser is the default paragraph parser with CanAcceptIndentedLine.
|
||||
// Used when disabling CodeBlockParser (as disabling it without a replacement will make indented blocks disappear).
|
||||
type indentableParagraphParser struct {
|
||||
parser.BlockParser
|
||||
}
|
||||
|
||||
var defaultIndentableParagraphParser = &indentableParagraphParser{BlockParser: parser.NewParagraphParser()}
|
||||
|
||||
func (b *indentableParagraphParser) CanAcceptIndentedLine() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var removeFeaturesExceptLinks = []any{
|
||||
parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
|
||||
)),
|
||||
format.Extensions, format.HTMLOptions, discordExtensions,
|
||||
goldmark.WithExtensions(&DiscordTag{portal}),
|
||||
)
|
||||
parser.NewSetextHeadingParser(), parser.NewATXHeadingParser(), parser.NewThematicBreakParser(),
|
||||
parser.NewCodeBlockParser(),
|
||||
}
|
||||
var removeFeaturesAndLinks = append(removeFeaturesExceptLinks, parser.NewLinkParser())
|
||||
var fixIndentedParagraphs = goldmark.WithParserOptions(parser.WithBlockParsers(util.Prioritized(defaultIndentableParagraphParser, 500)))
|
||||
var discordExtensions = goldmark.WithExtensions(extension.Strikethrough, mdext.SimpleSpoiler, mdext.DiscordUnderline, ExtDiscordEveryone, ExtDiscordTag)
|
||||
|
||||
var discordRenderer = goldmark.New(
|
||||
goldmark.WithParser(mdext.ParserWithoutFeatures(removeFeaturesAndLinks...)),
|
||||
fixIndentedParagraphs, format.HTMLOptions, discordExtensions,
|
||||
)
|
||||
var discordRendererWithInlineLinks = goldmark.New(
|
||||
goldmark.WithParser(mdext.ParserWithoutFeatures(removeFeaturesExceptLinks...)),
|
||||
fixIndentedParagraphs, format.HTMLOptions, discordExtensions,
|
||||
)
|
||||
|
||||
func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string, allowInlineLinks bool) string {
|
||||
text = escapeFixer.ReplaceAllStringFunc(text, escapeReplacement)
|
||||
|
||||
var buf strings.Builder
|
||||
err := mdRenderer.Convert([]byte(text), &buf)
|
||||
ctx := parser.NewContext()
|
||||
ctx.Set(parserContextPortal, portal)
|
||||
renderer := discordRenderer
|
||||
if allowInlineLinks {
|
||||
renderer = discordRendererWithInlineLinks
|
||||
}
|
||||
err := renderer.Convert([]byte(text), &buf, parser.WithContext(ctx))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("markdown parser errored: %w", err))
|
||||
}
|
||||
|
||||
@@ -96,9 +96,11 @@ func (r *discordEveryoneHTMLRenderer) renderDiscordEveryone(w util.BufWriter, so
|
||||
return
|
||||
}
|
||||
|
||||
type DiscordEveryone struct{}
|
||||
type discordEveryone struct{}
|
||||
|
||||
func (e *DiscordEveryone) Extend(m goldmark.Markdown) {
|
||||
var ExtDiscordEveryone = &discordEveryone{}
|
||||
|
||||
func (e *discordEveryone) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(defaultDiscordEveryoneParser, 600),
|
||||
))
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
|
||||
type astDiscordTag struct {
|
||||
ast.BaseInline
|
||||
portal *Portal
|
||||
id int64
|
||||
}
|
||||
|
||||
@@ -143,7 +144,10 @@ func (s *discordTagParser) Trigger() []byte {
|
||||
return []byte{'<'}
|
||||
}
|
||||
|
||||
var parserContextPortal = parser.NewContextKey()
|
||||
|
||||
func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||
portal := pc.Get(parserContextPortal).(*Portal)
|
||||
//before := block.PrecendingCharacter()
|
||||
line, _ := block.PeekLine()
|
||||
match := discordTagRegex.FindSubmatch(line)
|
||||
@@ -157,7 +161,7 @@ func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.C
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
tag := astDiscordTag{id: id}
|
||||
tag := astDiscordTag{id: id, portal: portal}
|
||||
tagName := string(match[1])
|
||||
switch {
|
||||
case tagName == "@":
|
||||
@@ -199,9 +203,9 @@ func (s *discordTagParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
type discordTagHTMLRenderer struct {
|
||||
portal *Portal
|
||||
}
|
||||
type discordTagHTMLRenderer struct{}
|
||||
|
||||
var defaultDiscordTagHTMLRenderer = &discordTagHTMLRenderer{}
|
||||
|
||||
func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(astKindDiscordTag, r.renderDiscordMention)
|
||||
@@ -259,17 +263,17 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
||||
}
|
||||
switch node := n.(type) {
|
||||
case *astDiscordUserMention:
|
||||
puppet := r.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10))
|
||||
puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10))
|
||||
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Name)
|
||||
return
|
||||
case *astDiscordRoleMention:
|
||||
role := r.portal.bridge.DB.Role.GetByID(r.portal.GuildID, strconv.FormatInt(node.id, 10))
|
||||
role := node.portal.bridge.DB.Role.GetByID(node.portal.GuildID, strconv.FormatInt(node.id, 10))
|
||||
if role != nil {
|
||||
_, _ = fmt.Fprintf(w, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name)
|
||||
return
|
||||
}
|
||||
case *astDiscordChannelMention:
|
||||
portal := r.portal.bridge.GetExistingPortalByID(database.PortalKey{
|
||||
portal := node.portal.bridge.GetExistingPortalByID(database.PortalKey{
|
||||
ChannelID: strconv.FormatInt(node.id, 10),
|
||||
Receiver: "",
|
||||
})
|
||||
@@ -282,7 +286,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
||||
return
|
||||
}
|
||||
case *astDiscordCustomEmoji:
|
||||
reactionMXC := r.portal.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated)
|
||||
reactionMXC := node.portal.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated)
|
||||
if !reactionMXC.IsEmpty() {
|
||||
_, _ = fmt.Fprintf(w, `<img data-mx-emoticon src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name)
|
||||
return
|
||||
@@ -310,15 +314,15 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
||||
return
|
||||
}
|
||||
|
||||
type DiscordTag struct {
|
||||
Portal *Portal
|
||||
}
|
||||
type discordTag struct{}
|
||||
|
||||
func (e *DiscordTag) Extend(m goldmark.Markdown) {
|
||||
var ExtDiscordTag = &discordTag{}
|
||||
|
||||
func (e *discordTag) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(defaultDiscordTagParser, 600),
|
||||
))
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(&discordTagHTMLRenderer{e.Portal}, 600),
|
||||
util.Prioritized(defaultDiscordTagHTMLRenderer, 600),
|
||||
))
|
||||
}
|
||||
|
||||
16
go.mod
16
go.mod
@@ -3,7 +3,7 @@ module go.mau.fi/mautrix-discord
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.26.1
|
||||
github.com/bwmarrin/discordgo v0.27.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/mux v1.8.0
|
||||
@@ -12,9 +12,9 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/yuin/goldmark v1.5.3
|
||||
github.com/yuin/goldmark v1.5.4
|
||||
maunium.net/go/maulogger/v2 v2.3.2
|
||||
maunium.net/go/mautrix v0.13.1-0.20230129131014-888cfabd8a52
|
||||
maunium.net/go/mautrix v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -22,16 +22,16 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/zerolog v1.28.0 // indirect
|
||||
github.com/rs/zerolog v1.29.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // 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
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/net v0.6.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // 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-20230129125832-37978ff8e399
|
||||
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230215201850-32771907474d
|
||||
|
||||
28
go.sum
28
go.sum
@@ -1,6 +1,6 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/beeper/discordgo v0.0.0-20230129125832-37978ff8e399 h1:3GZhhiyeXo/r40NmaQddBpCfosSSIrSrqZBLXJWrtYc=
|
||||
github.com/beeper/discordgo v0.0.0-20230129125832-37978ff8e399/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/beeper/discordgo v0.0.0-20230215201850-32771907474d h1:PndQKe7wiuQuVIWepQksfaRWUxZcoh6GWLXfWbdAN3g=
|
||||
github.com/beeper/discordgo v0.0.0-20230215201850-32771907474d/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -27,8 +27,8 @@ 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.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -47,22 +47,22 @@ 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.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
|
||||
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -77,5 +77,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.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
||||
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||
maunium.net/go/mautrix v0.13.1-0.20230129131014-888cfabd8a52 h1:7KoJL/7eozYlu4GW2jADHO+Qhm8WL45Afcm7A45BivM=
|
||||
maunium.net/go/mautrix v0.13.1-0.20230129131014-888cfabd8a52/go.mod h1:gYMQPsZ9lQpyKlVp+DGwOuc9LIcE/c8GZW2CvKHISgM=
|
||||
maunium.net/go/mautrix v0.14.0 h1:kdQ06HzmMaLGZqmSh/ykDhp5C2gIREQL9TS8hY+FqLs=
|
||||
maunium.net/go/mautrix v0.14.0/go.mod h1:voJPvnTkA60rxBl6mvdPxcP7y7iY5w3d/K55IoX+2oY=
|
||||
|
||||
7
main.go
7
main.go
@@ -23,6 +23,7 @@ import (
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/commands"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/util"
|
||||
"maunium.net/go/mautrix/util/configupgrade"
|
||||
|
||||
"go.mau.fi/mautrix-discord/config"
|
||||
@@ -71,6 +72,8 @@ type DiscordBridge struct {
|
||||
puppets map[string]*Puppet
|
||||
puppetsByCustomMXID map[id.UserID]*Puppet
|
||||
puppetsLock sync.Mutex
|
||||
|
||||
attachmentTransfers *util.SyncMap[attachmentKey, *util.ReturnableOnce[*database.File]]
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) GetExampleConfig() string {
|
||||
@@ -163,12 +166,14 @@ func main() {
|
||||
|
||||
puppets: make(map[string]*Puppet),
|
||||
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
||||
|
||||
attachmentTransfers: util.NewSyncMap[attachmentKey, *util.ReturnableOnce[*database.File]](),
|
||||
}
|
||||
br.Bridge = bridge.Bridge{
|
||||
Name: "mautrix-discord",
|
||||
URL: "https://github.com/mautrix/discord",
|
||||
Description: "A Matrix-Discord puppeting bridge.",
|
||||
Version: "0.1.0",
|
||||
Version: "0.1.1",
|
||||
ProtocolName: "Discord",
|
||||
|
||||
CryptoPickleKey: "maunium.net/go/mautrix-whatsapp",
|
||||
|
||||
143
portal.go
143
portal.go
@@ -555,7 +555,11 @@ func (portal *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridg
|
||||
const DiscordStickerSize = 160
|
||||
|
||||
func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, id, content.Info.MimeType)
|
||||
meta := AttachmentMeta{AttachmentID: id, MimeType: content.Info.MimeType}
|
||||
if typeName == "sticker" && content.Info.MimeType == "application/json" {
|
||||
meta.Converter = portal.bridge.convertLottie
|
||||
}
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, meta)
|
||||
if err != nil {
|
||||
errorEventID := portal.sendMediaFailedMessage(intent, err)
|
||||
if errorEventID != "" {
|
||||
@@ -566,11 +570,18 @@ func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.Inte
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if typeName == "sticker" && content.Info.MimeType == "application/json" {
|
||||
content.Info.MimeType = dbFile.MimeType
|
||||
}
|
||||
content.Info.Size = dbFile.Size
|
||||
if content.Info.Width == 0 && content.Info.Height == 0 {
|
||||
content.Info.Width = dbFile.Width
|
||||
content.Info.Height = dbFile.Height
|
||||
}
|
||||
if content.Info.Width == 0 && content.Info.Height == 0 && typeName == "sticker" {
|
||||
content.Info.Width = DiscordStickerSize
|
||||
content.Info.Height = DiscordStickerSize
|
||||
}
|
||||
if dbFile.DecryptionInfo != nil {
|
||||
content.File = &event.EncryptedFileInfo{
|
||||
EncryptedFile: *dbFile.DecryptionInfo,
|
||||
@@ -675,7 +686,7 @@ type ConvertedMessage struct {
|
||||
}
|
||||
|
||||
func (portal *Portal) convertDiscordVideoEmbed(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *ConvertedMessage {
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, "", "")
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, NoMeta)
|
||||
if err != nil {
|
||||
return &ConvertedMessage{Content: portal.createMediaFailedMessage(err)}
|
||||
}
|
||||
@@ -740,7 +751,7 @@ func (portal *Portal) handleDiscordVideoEmbed(intent *appservice.IntentAPI, embe
|
||||
const (
|
||||
embedHTMLWrapper = `<blockquote class="discord-embed">%s</blockquote>`
|
||||
embedHTMLWrapperColor = `<blockquote class="discord-embed" background-color="#%06X">%s</blockquote>`
|
||||
embedHTMLAuthorWithImage = `<p class="discord-embed-author"><img data-mx-emoticon width="24" height="24" src="%s" title="Author icon" alt=""> <span>%s</span></p>`
|
||||
embedHTMLAuthorWithImage = `<p class="discord-embed-author"><img data-mx-emoticon height="24" src="%s" title="Author icon" alt=""> <span>%s</span></p>`
|
||||
embedHTMLAuthorPlain = `<p class="discord-embed-author"><span>%s</span></p>`
|
||||
embedHTMLAuthorLink = `<a href="%s">%s</a>`
|
||||
embedHTMLTitleWithLink = `<p class="discord-embed-title"><a href="%s"><strong>%s</strong></a></p>`
|
||||
@@ -751,7 +762,7 @@ const (
|
||||
embedHTMLFields = `<table class="discord-embed-fields"><tr>%s</tr><tr>%s</tr></table>`
|
||||
embedHTMLLinearField = `<p class="discord-embed-field" x-inline="%s"><strong>%s</strong><br><span>%s</span></p>`
|
||||
embedHTMLImage = `<p class="discord-embed-image"><img src="%s" alt="" title="Embed image"></p>`
|
||||
embedHTMLFooterWithImage = `<p class="discord-embed-footer"><sub><img data-mx-emoticon width="20" height="20" src="%s" title="Footer icon" alt=""> <span>%s</span>%s</sub></p>`
|
||||
embedHTMLFooterWithImage = `<p class="discord-embed-footer"><sub><img data-mx-emoticon height="20" src="%s" title="Footer icon" alt=""> <span>%s</span>%s</sub></p>`
|
||||
embedHTMLFooterPlain = `<p class="discord-embed-footer"><sub><span>%s</span>%s</sub></p>`
|
||||
embedHTMLFooterOnlyDate = `<p class="discord-embed-footer"><sub>%s</sub></p>`
|
||||
embedHTMLDate = `<time datetime="%s">%s</time>`
|
||||
@@ -768,7 +779,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
|
||||
}
|
||||
authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML)
|
||||
if embed.Author.ProxyIconURL != "" {
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Author.ProxyIconURL, false, "", "")
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Author.ProxyIconURL, false, NoMeta)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to reupload author icon in embed #%d of message %s: %v", index+1, msgID, err)
|
||||
} else {
|
||||
@@ -779,7 +790,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
|
||||
}
|
||||
if embed.Title != "" {
|
||||
var titleHTML string
|
||||
baseTitleHTML := portal.renderDiscordMarkdownOnlyHTML(embed.Title)
|
||||
baseTitleHTML := portal.renderDiscordMarkdownOnlyHTML(embed.Title, false)
|
||||
if embed.URL != "" {
|
||||
titleHTML = fmt.Sprintf(embedHTMLTitleWithLink, html.EscapeString(embed.URL), baseTitleHTML)
|
||||
} else {
|
||||
@@ -788,7 +799,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
|
||||
htmlParts = append(htmlParts, titleHTML)
|
||||
}
|
||||
if embed.Description != "" {
|
||||
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLDescription, portal.renderDiscordMarkdownOnlyHTML(embed.Description)))
|
||||
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLDescription, portal.renderDiscordMarkdownOnlyHTML(embed.Description, true)))
|
||||
}
|
||||
for i := 0; i < len(embed.Fields); i++ {
|
||||
item := embed.Fields[i]
|
||||
@@ -805,20 +816,20 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
|
||||
headerParts := make([]string, len(splitItems))
|
||||
contentParts := make([]string, len(splitItems))
|
||||
for j, splitItem := range splitItems {
|
||||
headerParts[j] = fmt.Sprintf(embedHTMLFieldName, portal.renderDiscordMarkdownOnlyHTML(splitItem.Name))
|
||||
contentParts[j] = fmt.Sprintf(embedHTMLFieldValue, portal.renderDiscordMarkdownOnlyHTML(splitItem.Value))
|
||||
headerParts[j] = fmt.Sprintf(embedHTMLFieldName, portal.renderDiscordMarkdownOnlyHTML(splitItem.Name, false))
|
||||
contentParts[j] = fmt.Sprintf(embedHTMLFieldValue, portal.renderDiscordMarkdownOnlyHTML(splitItem.Value, true))
|
||||
}
|
||||
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLFields, strings.Join(headerParts, ""), strings.Join(contentParts, "")))
|
||||
} else {
|
||||
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLLinearField,
|
||||
strconv.FormatBool(item.Inline),
|
||||
portal.renderDiscordMarkdownOnlyHTML(item.Name),
|
||||
portal.renderDiscordMarkdownOnlyHTML(item.Value),
|
||||
portal.renderDiscordMarkdownOnlyHTML(item.Name, false),
|
||||
portal.renderDiscordMarkdownOnlyHTML(item.Value, true),
|
||||
))
|
||||
}
|
||||
}
|
||||
if embed.Image != nil {
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Image.ProxyURL, false, "", "")
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Image.ProxyURL, false, NoMeta)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to reupload image in embed #%d of message %s: %v", index+1, msgID, err)
|
||||
} else {
|
||||
@@ -844,7 +855,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe
|
||||
}
|
||||
footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart)
|
||||
if embed.Footer.ProxyIconURL != "" {
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Footer.ProxyIconURL, false, "", "")
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Footer.ProxyIconURL, false, NoMeta)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to reupload footer icon in embed #%d of message %s: %v", index+1, msgID, err)
|
||||
} else {
|
||||
@@ -876,7 +887,7 @@ type BeeperLinkPreview struct {
|
||||
}
|
||||
|
||||
func (portal *Portal) convertDiscordLinkEmbedImage(intent *appservice.IntentAPI, url string, width, height int, preview *BeeperLinkPreview) {
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, "", "")
|
||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, NoMeta)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to copy image in URL preview: %v", err)
|
||||
} else {
|
||||
@@ -919,7 +930,51 @@ const msgInteractionTemplateHTML = `<blockquote>
|
||||
|
||||
const msgComponentTemplateHTML = `<p>This message contains interactive elements. Use the Discord app to interact with the message.</p>`
|
||||
|
||||
type BridgeEmbedType int
|
||||
|
||||
const (
|
||||
EmbedUnknown BridgeEmbedType = iota
|
||||
EmbedRich
|
||||
EmbedLinkPreview
|
||||
EmbedVideo
|
||||
)
|
||||
|
||||
func isActuallyLinkPreview(embed *discordgo.MessageEmbed) bool {
|
||||
// Sending YouTube links creates a video embed, but we want to bridge it as a URL preview,
|
||||
// so this is a hacky way to detect those.
|
||||
return embed.Video != nil && embed.Video.ProxyURL == ""
|
||||
}
|
||||
|
||||
func getEmbedType(embed *discordgo.MessageEmbed) BridgeEmbedType {
|
||||
switch embed.Type {
|
||||
case discordgo.EmbedTypeLink, discordgo.EmbedTypeArticle:
|
||||
return EmbedLinkPreview
|
||||
case discordgo.EmbedTypeVideo:
|
||||
if isActuallyLinkPreview(embed) {
|
||||
return EmbedLinkPreview
|
||||
}
|
||||
return EmbedVideo
|
||||
case discordgo.EmbedTypeGifv:
|
||||
return EmbedVideo
|
||||
case discordgo.EmbedTypeRich, discordgo.EmbedTypeImage:
|
||||
return EmbedRich
|
||||
default:
|
||||
return EmbedUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) convertDiscordTextMessage(intent *appservice.IntentAPI, msg *discordgo.Message, relation *event.RelatesTo, isEdit bool) *ConvertedMessage {
|
||||
if msg.Type == discordgo.MessageTypeCall {
|
||||
return &ConvertedMessage{Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgEmote,
|
||||
Body: "started a call",
|
||||
}}
|
||||
} else if msg.Type == discordgo.MessageTypeGuildMemberJoin {
|
||||
return &ConvertedMessage{Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgEmote,
|
||||
Body: "joined the server",
|
||||
}}
|
||||
}
|
||||
var htmlParts []string
|
||||
if msg.Interaction != nil {
|
||||
puppet := portal.bridge.GetPuppetByID(msg.Interaction.User.ID)
|
||||
@@ -927,16 +982,16 @@ func (portal *Portal) convertDiscordTextMessage(intent *appservice.IntentAPI, ms
|
||||
htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, puppet.MXID, puppet.Name, msg.Interaction.Name))
|
||||
}
|
||||
if msg.Content != "" && !isPlainGifMessage(msg) {
|
||||
htmlParts = append(htmlParts, portal.renderDiscordMarkdownOnlyHTML(msg.Content))
|
||||
htmlParts = append(htmlParts, portal.renderDiscordMarkdownOnlyHTML(msg.Content, false))
|
||||
}
|
||||
previews := make([]*BeeperLinkPreview, 0)
|
||||
for i, embed := range msg.Embeds {
|
||||
switch embed.Type {
|
||||
case discordgo.EmbedTypeRich, discordgo.EmbedTypeImage:
|
||||
switch getEmbedType(embed) {
|
||||
case EmbedRich:
|
||||
htmlParts = append(htmlParts, portal.convertDiscordRichEmbed(intent, embed, msg.ID, i))
|
||||
case discordgo.EmbedTypeLink, discordgo.EmbedTypeArticle:
|
||||
case EmbedLinkPreview:
|
||||
previews = append(previews, portal.convertDiscordLinkEmbedToBeeper(intent, embed))
|
||||
case discordgo.EmbedTypeVideo, discordgo.EmbedTypeGifv:
|
||||
case EmbedVideo:
|
||||
// Ignore video embeds, they're handled as separate messages
|
||||
default:
|
||||
portal.log.Warnfln("Unknown type %s in embed #%d of message %s", embed.Type, i+1, msg.ID)
|
||||
@@ -1057,7 +1112,7 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
|
||||
handledURLs := make(map[string]struct{})
|
||||
for i, embed := range msg.Embeds {
|
||||
// Ignore non-video embeds, they're handled in convertDiscordTextMessage
|
||||
if embed.Type != discordgo.EmbedTypeVideo && embed.Type != discordgo.EmbedTypeGifv {
|
||||
if getEmbedType(embed) != EmbedVideo {
|
||||
continue
|
||||
}
|
||||
// Discord deduplicates embeds by URL. It makes things easier for us too.
|
||||
@@ -1144,6 +1199,9 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
|
||||
if !ok {
|
||||
portal.log.Debugfln("Dropping edit with no author of non-recent message %s", msg.ID)
|
||||
return
|
||||
} else if creationMessage.Type == discordgo.MessageTypeCall {
|
||||
portal.log.Debugfln("Dropping edit of call message %s", msg.ID)
|
||||
return
|
||||
}
|
||||
portal.log.Debugfln("Found original message %s in cache for edit without author", msg.ID)
|
||||
if len(msg.Embeds) > 0 {
|
||||
@@ -1181,7 +1239,7 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
|
||||
}
|
||||
for _, remainingEmbed := range msg.Embeds {
|
||||
// Other types of embeds are sent inline with the text message part
|
||||
if remainingEmbed.Type != discordgo.EmbedTypeVideo && remainingEmbed.Type != discordgo.EmbedTypeGifv {
|
||||
if getEmbedType(remainingEmbed) != EmbedVideo {
|
||||
continue
|
||||
}
|
||||
embedID := "video_" + remainingEmbed.URL
|
||||
@@ -1210,12 +1268,11 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
|
||||
return
|
||||
}
|
||||
converted.Content.SetEdit(existing[0].MXID)
|
||||
extraContentCopy := map[string]any{}
|
||||
for key, value := range converted.Extra {
|
||||
extraContentCopy[key] = value
|
||||
if converted.Extra != nil {
|
||||
converted.Extra = map[string]any{
|
||||
"m.new_content": converted.Extra,
|
||||
}
|
||||
}
|
||||
extraContentCopy["m.new_content"] = converted.Extra
|
||||
converted.Extra = extraContentCopy
|
||||
|
||||
var editTS int64
|
||||
if msg.EditedTimestamp != nil {
|
||||
@@ -1252,6 +1309,24 @@ func (portal *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Mess
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) handleDiscordTyping(evt *discordgo.TypingStart) {
|
||||
puppet := portal.bridge.GetPuppetByID(evt.UserID)
|
||||
if puppet.Name == "" {
|
||||
// Puppet hasn't been synced yet
|
||||
return
|
||||
}
|
||||
intent := puppet.IntentFor(portal)
|
||||
err := intent.EnsureJoined(portal.MXID)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to ensure %s is joined for typing notification: %v", puppet.MXID, portal.MXID, err)
|
||||
return
|
||||
}
|
||||
_, err = intent.UserTyping(portal.MXID, true, 12*time.Second)
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to mark %s as typing: %v", puppet.MXID, portal.MXID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) syncParticipants(source *User, participants []*discordgo.User) {
|
||||
for _, participant := range participants {
|
||||
puppet := portal.bridge.GetPuppetByID(participant.ID)
|
||||
@@ -1576,7 +1651,7 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
|
||||
}
|
||||
sendReq.Content = portal.parseMatrixHTML(sender, content)
|
||||
case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo:
|
||||
data, err := portal.downloadMatrixAttachment(content)
|
||||
data, err := downloadMatrixAttachment(portal.MainIntent(), content)
|
||||
if err != nil {
|
||||
go portal.sendMessageMetrics(evt, err, "Error downloading media in")
|
||||
return
|
||||
@@ -1821,13 +1896,13 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) {
|
||||
emojiID := reaction.RelatesTo.Key
|
||||
if strings.HasPrefix(emojiID, "mxc://") {
|
||||
uri, _ := id.ParseContentURI(emojiID)
|
||||
emoji := portal.bridge.DB.Emoji.GetByMatrixURL(uri)
|
||||
if emoji == nil {
|
||||
emojiFile := portal.bridge.DB.File.GetByMXC(uri)
|
||||
if emojiFile == nil || emojiFile.ID == "" || emojiFile.EmojiName == "" {
|
||||
go portal.sendMessageMetrics(evt, fmt.Errorf("%w %s", errUnknownEmoji, emojiID), "Ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
emojiID = emoji.APIName()
|
||||
emojiID = fmt.Sprintf("%s:%s", emojiFile.EmojiName, emojiFile.ID)
|
||||
} else {
|
||||
emojiID = variationselector.Remove(emojiID)
|
||||
}
|
||||
@@ -1855,7 +1930,11 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) {
|
||||
}
|
||||
|
||||
func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageReaction, add bool, thread *Thread) {
|
||||
intent := portal.bridge.GetPuppetByID(reaction.UserID).IntentFor(portal)
|
||||
puppet := portal.bridge.GetPuppetByID(reaction.UserID)
|
||||
if reaction.Member != nil && reaction.Member.User != nil {
|
||||
puppet.UpdateInfo(nil, reaction.Member.User)
|
||||
}
|
||||
intent := puppet.IntentFor(portal)
|
||||
|
||||
var discordID string
|
||||
var matrixReaction string
|
||||
@@ -1866,7 +1945,7 @@ func (portal *Portal) handleDiscordReaction(user *User, reaction *discordgo.Mess
|
||||
return
|
||||
}
|
||||
matrixReaction = reactionMXC.String()
|
||||
discordID = reaction.Emoji.ID
|
||||
discordID = fmt.Sprintf("%s:%s", reaction.Emoji.Name, reaction.Emoji.ID)
|
||||
} else {
|
||||
discordID = reaction.Emoji.Name
|
||||
matrixReaction = variationselector.Add(reaction.Emoji.Name)
|
||||
|
||||
49
user.go
49
user.go
@@ -521,6 +521,7 @@ func (user *User) Connect() error {
|
||||
user.Session = session
|
||||
|
||||
user.Session.AddHandler(user.readyHandler)
|
||||
user.Session.AddHandler(user.resumeHandler)
|
||||
user.Session.AddHandler(user.connectedHandler)
|
||||
user.Session.AddHandler(user.disconnectedHandler)
|
||||
user.Session.AddHandler(user.invalidAuthHandler)
|
||||
@@ -613,9 +614,36 @@ func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
|
||||
user.Update()
|
||||
}
|
||||
|
||||
go user.subscribeGuilds(2 * time.Second)
|
||||
|
||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
}
|
||||
|
||||
func (user *User) subscribeGuilds(delay time.Duration) {
|
||||
for _, guildMeta := range user.Session.State.Guilds {
|
||||
guild := user.bridge.GetGuildByID(guildMeta.ID, false)
|
||||
if guild != nil && guild.MXID != "" {
|
||||
user.log.Debugfln("Subscribing to guild %s", guild.ID)
|
||||
dat := discordgo.GuildSubscribeData{
|
||||
GuildID: guild.ID,
|
||||
Typing: true,
|
||||
Activities: true,
|
||||
Threads: true,
|
||||
}
|
||||
err := user.Session.SubscribeGuild(dat)
|
||||
if err != nil {
|
||||
user.log.Warnfln("Failed to subscribe to %s: %v", guild.ID, err)
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (user *User) resumeHandler(_ *discordgo.Session, r *discordgo.Resumed) {
|
||||
user.log.Debugln("Discord connection resumed")
|
||||
user.subscribeGuilds(0 * time.Second)
|
||||
}
|
||||
|
||||
func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
|
||||
if portal.MXID == "" {
|
||||
return false
|
||||
@@ -879,6 +907,10 @@ func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildI
|
||||
}
|
||||
portal = thread.Parent
|
||||
}
|
||||
// Double check because some messages don't have the guild ID specified.
|
||||
if !user.bridgeMessage(portal.GuildID) {
|
||||
return
|
||||
}
|
||||
|
||||
portal.discordMessages <- portalDiscordMessage{
|
||||
msg: msg,
|
||||
@@ -963,11 +995,7 @@ func (user *User) typingStartHandler(_ *discordgo.Session, t *discordgo.TypingSt
|
||||
if portal == nil || portal.MXID == "" {
|
||||
return
|
||||
}
|
||||
puppet := user.bridge.GetPuppetByID(t.UserID)
|
||||
_, err := puppet.IntentFor(portal).UserTyping(portal.MXID, true, 12*time.Second)
|
||||
if err != nil {
|
||||
user.log.Warnfln("Failed to mark %s as typing in %s: %v", puppet.MXID, portal.MXID, err)
|
||||
}
|
||||
portal.handleDiscordTyping(t)
|
||||
}
|
||||
|
||||
func (user *User) interactionSuccessHandler(_ *discordgo.Session, s *discordgo.InteractionSuccess) {
|
||||
@@ -1125,6 +1153,17 @@ func (user *User) bridgeGuild(guildID string, everything bool) error {
|
||||
guild.AutoBridgeChannels = everything
|
||||
guild.Update()
|
||||
|
||||
user.log.Debugfln("Subscribing to guild %s after bridging", guild.ID)
|
||||
err = user.Session.SubscribeGuild(discordgo.GuildSubscribeData{
|
||||
GuildID: guild.ID,
|
||||
Typing: true,
|
||||
Activities: true,
|
||||
Threads: true,
|
||||
})
|
||||
if err != nil {
|
||||
user.log.Warnfln("Failed to subscribe to %s: %v", guild.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user