Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69268f8d92 | ||
|
|
05bc4f9312 | ||
|
|
f5ef87eb83 | ||
|
|
3cdf018c37 | ||
|
|
46115fafd5 | ||
|
|
15d4cf07f9 | ||
|
|
ff052d7f18 | ||
|
|
ef7e77515a | ||
|
|
0deec8b853 | ||
|
|
d42c4722c9 | ||
|
|
ee2ad7527e | ||
|
|
5a40f0a2ab | ||
|
|
c163fba712 | ||
|
|
9c87532d52 | ||
|
|
9b63defbe8 | ||
|
|
4e9e50dbed | ||
|
|
3c52e76e15 | ||
|
|
0e8b845014 | ||
|
|
f8bbcc9080 | ||
|
|
febb28882e | ||
|
|
0403a705b6 | ||
|
|
2440ca4e83 | ||
|
|
39096c9347 | ||
|
|
72d4fb755b | ||
|
|
7bfa885530 | ||
|
|
f7c8e03041 | ||
|
|
d3828f2fb3 | ||
|
|
bccdc67eb2 | ||
|
|
c625ee3ba7 | ||
|
|
17d4b79554 | ||
|
|
6365db46cc | ||
|
|
af52979669 | ||
|
|
ccd29752c7 | ||
|
|
4eba894573 | ||
|
|
71d1689776 | ||
|
|
ce4d05bb11 | ||
|
|
681a5ff2ab | ||
|
|
60c260a471 | ||
|
|
efd22e33b5 | ||
|
|
7b5c057dcf | ||
|
|
a0cc5ec9bc | ||
|
|
77b230f4d8 | ||
|
|
cace8b5939 | ||
|
|
ac7ad471a5 | ||
|
|
a6c3b84db5 | ||
|
|
4676ec98c4 | ||
|
|
541c8e1169 | ||
|
|
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:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
go-version: [1.19]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Go ${{ matrix.go-version }}
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: "1.20"
|
||||||
|
|
||||||
- name: Install libolm
|
- name: Install libolm
|
||||||
run: sudo apt-get install libolm-dev libolm3
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
config.yaml
|
*.yaml
|
||||||
discord
|
!example-config.yaml
|
||||||
logs/
|
!.pre-commit-config.yaml
|
||||||
registration.yaml
|
|
||||||
*.db*
|
*.db*
|
||||||
|
*.log*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.1.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude_types: [markdown]
|
exclude_types: [markdown]
|
||||||
@@ -9,7 +9,7 @@ repos:
|
|||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
|
|
||||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||||
rev: v1.0.0-beta.5
|
rev: v1.0.0-rc.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-imports-repo
|
- id: go-imports-repo
|
||||||
- id: go-vet-repo-mod
|
- id: go-vet-repo-mod
|
||||||
|
|||||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
|||||||
|
# v0.2.0 (2023-03-16)
|
||||||
|
|
||||||
|
* Switched to zerolog for logging.
|
||||||
|
* The basic log config will be migrated automatically, but you may want to
|
||||||
|
tweak it as the options are different.
|
||||||
|
* Added support for logging in with a bot account.
|
||||||
|
The [Authentication docs](https://docs.mau.fi/bridges/go/discord/authentication.html)
|
||||||
|
have been updated with instructions for creating a bot.
|
||||||
|
* Added support for relaying messages for unauthenticated users using a webhook.
|
||||||
|
See [docs](https://docs.mau.fi/bridges/go/discord/relay.html) for instructions.
|
||||||
|
* Added commands to bridge and unbridge channels manually.
|
||||||
|
* Added `ping` command.
|
||||||
|
* Added support for gif stickers from Discord.
|
||||||
|
* Changed mention bridging so mentions for users logged into the bridge use the
|
||||||
|
Matrix user's MXID even if double puppeting is not enabled.
|
||||||
|
* Actually fixed ghost user info not being synced when receiving reactions.
|
||||||
|
* Fixed uncommon bug with sending messages that only occurred after login
|
||||||
|
before restarting the bridge.
|
||||||
|
* Fixed guild name not being synced immediately after joining a new guild.
|
||||||
|
* Fixed variation selectors when bridging emojis to Discord.
|
||||||
|
|
||||||
|
# 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)
|
# v0.1.0 (2023-01-29)
|
||||||
|
|
||||||
Initial release.
|
Initial release.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie
|
||||||
|
|
||||||
FROM golang:1-alpine3.17 AS builder
|
FROM golang:1-alpine3.17 AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
||||||
@@ -11,8 +13,11 @@ FROM alpine:3.17
|
|||||||
ENV UID=1337 \
|
ENV UID=1337 \
|
||||||
GID=1337
|
GID=1337
|
||||||
|
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq 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 /usr/bin/mautrix-discord /usr/bin/mautrix-discord
|
||||||
COPY --from=builder /build/example-config.yaml /opt/mautrix-discord/example-config.yaml
|
COPY --from=builder /build/example-config.yaml /opt/mautrix-discord/example-config.yaml
|
||||||
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie
|
||||||
|
|
||||||
FROM alpine:3.17
|
FROM alpine:3.17
|
||||||
|
|
||||||
ENV UID=1337 \
|
ENV UID=1337 \
|
||||||
GID=1337
|
GID=1337
|
||||||
|
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq \
|
||||||
|
zlib libpng giflib libstdc++ libgcc
|
||||||
|
|
||||||
|
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
|
||||||
|
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
|
||||||
ARG EXECUTABLE=./mautrix-discord
|
ARG EXECUTABLE=./mautrix-discord
|
||||||
COPY $EXECUTABLE /usr/bin/mautrix-discord
|
COPY $EXECUTABLE /usr/bin/mautrix-discord
|
||||||
COPY ./example-config.yaml /opt/mautrix-discord/example-config.yaml
|
COPY ./example-config.yaml /opt/mautrix-discord/example-config.yaml
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17 AS lottie
|
||||||
|
|
||||||
FROM golang:1-alpine3.17 AS builder
|
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
|
COPY . /build
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN go build -o /usr/bin/mautrix-discord
|
RUN go build -o /usr/bin/mautrix-discord
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ All setup and usage instructions are located on [docs.mau.fi]. Some quick links:
|
|||||||
|
|
||||||
* [Bridge setup](https://docs.mau.fi/bridges/go/setup.html?bridge=discord)
|
* [Bridge setup](https://docs.mau.fi/bridges/go/setup.html?bridge=discord)
|
||||||
(or [with Docker](https://docs.mau.fi/bridges/general/docker-setup.html?bridge=discord))
|
(or [with Docker](https://docs.mau.fi/bridges/general/docker-setup.html?bridge=discord))
|
||||||
* Basic usage: [Authentication](https://docs.mau.fi/bridges/go/discord/authentication.html)
|
* Basic usage: [Authentication](https://docs.mau.fi/bridges/go/discord/authentication.html),
|
||||||
|
[Relaying with webhooks](https://docs.mau.fi/bridges/go/discord/relay.html)
|
||||||
|
|
||||||
### Features & Roadmap
|
### Features & Roadmap
|
||||||
[ROADMAP.md](https://github.com/mautrix/discord/blob/main/ROADMAP.md)
|
[ROADMAP.md](https://github.com/mautrix/discord/blob/main/ROADMAP.md)
|
||||||
|
|||||||
190
attachments.go
190
attachments.go
@@ -2,10 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,6 +21,9 @@ import (
|
|||||||
"maunium.net/go/mautrix/appservice"
|
"maunium.net/go/mautrix/appservice"
|
||||||
"maunium.net/go/mautrix/crypto/attachment"
|
"maunium.net/go/mautrix/crypto/attachment"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util"
|
||||||
|
"maunium.net/go/mautrix/util/ffmpeg"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
"go.mau.fi/mautrix-discord/database"
|
||||||
)
|
)
|
||||||
@@ -62,7 +70,7 @@ func uploadDiscordAttachment(url string, data []byte) error {
|
|||||||
return nil
|
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
|
var file *event.EncryptedFileInfo
|
||||||
rawMXC := content.URL
|
rawMXC := content.URL
|
||||||
|
|
||||||
@@ -76,7 +84,7 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := portal.MainIntent().DownloadBytes(mxc)
|
data, err := intent.DownloadBytes(mxc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -91,23 +99,24 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten
|
|||||||
return data, nil
|
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 := br.DB.File.New()
|
||||||
dbFile.Timestamp = time.Now()
|
dbFile.Timestamp = time.Now()
|
||||||
dbFile.URL = url
|
dbFile.URL = url
|
||||||
dbFile.ID = attachmentID
|
dbFile.ID = meta.AttachmentID
|
||||||
|
dbFile.EmojiName = meta.EmojiName
|
||||||
dbFile.Size = len(data)
|
dbFile.Size = len(data)
|
||||||
dbFile.MimeType = mimetype.Detect(data).String()
|
dbFile.MimeType = mimetype.Detect(data).String()
|
||||||
if mime == "" {
|
if meta.MimeType == "" {
|
||||||
mime = dbFile.MimeType
|
meta.MimeType = dbFile.MimeType
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(mime, "image/") {
|
if strings.HasPrefix(meta.MimeType, "image/") {
|
||||||
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
||||||
dbFile.Width = cfg.Width
|
dbFile.Width = cfg.Width
|
||||||
dbFile.Height = cfg.Height
|
dbFile.Height = cfg.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadMime := mime
|
uploadMime := meta.MimeType
|
||||||
if encrypt {
|
if encrypt {
|
||||||
dbFile.Encrypted = true
|
dbFile.Encrypted = true
|
||||||
dbFile.DecryptionInfo = attachment.NewEncryptedFile()
|
dbFile.DecryptionInfo = attachment.NewEncryptedFile()
|
||||||
@@ -140,22 +149,161 @@ func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, da
|
|||||||
}
|
}
|
||||||
dbFile.MXC = uploaded.ContentURI
|
dbFile.MXC = uploaded.ContentURI
|
||||||
}
|
}
|
||||||
dbFile.Insert(nil)
|
|
||||||
return dbFile, nil
|
return dbFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, attachmentID, mime string) (*database.File, error) {
|
type AttachmentMeta struct {
|
||||||
dbFile := br.DB.File.Get(url, encrypt)
|
AttachmentID string
|
||||||
if dbFile == nil {
|
MimeType string
|
||||||
data, err := downloadDiscordAttachment(url)
|
EmojiName string
|
||||||
if err != nil {
|
CopyIfMissing bool
|
||||||
return nil, err
|
Converter func([]byte) ([]byte, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbFile, err = br.uploadMatrixAttachment(intent, data, url, encrypt, attachmentID, mime)
|
var NoMeta = AttachmentMeta{}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
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)
|
||||||
}
|
}
|
||||||
return dbFile, nil
|
|
||||||
|
ctx := context.Background()
|
||||||
|
tempdir, err := os.MkdirTemp("", "mautrix_discord_lottie_")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
477
commands.go
477
commands.go
@@ -19,6 +19,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
@@ -30,10 +31,13 @@ import (
|
|||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
"maunium.net/go/mautrix/bridge/commands"
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-discord/database"
|
||||||
"go.mau.fi/mautrix-discord/remoteauth"
|
"go.mau.fi/mautrix-discord/remoteauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,14 +48,22 @@ type WrappedCommandEvent struct {
|
|||||||
Portal *Portal
|
Portal *Portal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var HelpSectionPortalManagement = commands.HelpSection{Name: "Portal management", Order: 20}
|
||||||
|
|
||||||
func (br *DiscordBridge) RegisterCommands() {
|
func (br *DiscordBridge) RegisterCommands() {
|
||||||
proc := br.CommandProcessor.(*commands.Processor)
|
proc := br.CommandProcessor.(*commands.Processor)
|
||||||
proc.AddHandlers(
|
proc.AddHandlers(
|
||||||
cmdLoginToken,
|
cmdLoginToken,
|
||||||
cmdLoginQR,
|
cmdLoginQR,
|
||||||
cmdLogout,
|
cmdLogout,
|
||||||
|
cmdPing,
|
||||||
cmdReconnect,
|
cmdReconnect,
|
||||||
cmdDisconnect,
|
cmdDisconnect,
|
||||||
|
cmdBridge,
|
||||||
|
cmdUnbridge,
|
||||||
|
cmdDeletePortal,
|
||||||
|
cmdSetRelay,
|
||||||
|
cmdUnsetRelay,
|
||||||
cmdGuilds,
|
cmdGuilds,
|
||||||
cmdRejoinSpace,
|
cmdRejoinSpace,
|
||||||
cmdDeleteAllPortals,
|
cmdDeleteAllPortals,
|
||||||
@@ -78,12 +90,45 @@ var cmdLoginToken = &commands.FullHandler{
|
|||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionAuth,
|
Section: commands.HelpSectionAuth,
|
||||||
Description: "Link the bridge to your Discord account by extracting the access token manually.",
|
Description: "Link the bridge to your Discord account by extracting the access token manually.",
|
||||||
|
Args: "<user/bot/oauth> <_token_>",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const discordTokenEpoch = 1293840000
|
||||||
|
|
||||||
|
func decodeToken(token string) (userID int64, err error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
err = fmt.Errorf("invalid number of parts in token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var userIDStr []byte
|
||||||
|
userIDStr, err = base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("invalid base64 in user ID part: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("invalid base64 in random part: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = base64.RawURLEncoding.DecodeString(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("invalid base64 in checksum part: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID, err = strconv.ParseInt(string(userIDStr), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("invalid number in decoded user ID part: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func fnLoginToken(ce *WrappedCommandEvent) {
|
func fnLoginToken(ce *WrappedCommandEvent) {
|
||||||
if len(ce.Args) == 0 {
|
if len(ce.Args) != 2 {
|
||||||
ce.Reply("**Usage**: `$cmdprefix login-token <token>`")
|
ce.Reply("**Usage**: `$cmdprefix login-token <user/bot/oauth> <token>`")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ce.MarkRead()
|
ce.MarkRead()
|
||||||
@@ -92,7 +137,25 @@ func fnLoginToken(ce *WrappedCommandEvent) {
|
|||||||
ce.Reply("You're already logged in")
|
ce.Reply("You're already logged in")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ce.User.Login(ce.Args[0]); err != nil {
|
token := ce.Args[1]
|
||||||
|
userID, err := decodeToken(token)
|
||||||
|
if err != nil {
|
||||||
|
ce.Reply("Invalid token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch strings.ToLower(ce.Args[0]) {
|
||||||
|
case "user":
|
||||||
|
// Token is used as-is
|
||||||
|
case "bot":
|
||||||
|
token = "Bot " + token
|
||||||
|
case "oauth":
|
||||||
|
token = "Bearer " + token
|
||||||
|
default:
|
||||||
|
ce.Reply("Token type must be `user`, `bot` or `oauth`")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ce.Reply("Connecting to Discord as user ID %d", userID)
|
||||||
|
if err = ce.User.Login(token); err != nil {
|
||||||
ce.Reply("Error connecting to Discord: %v", err)
|
ce.Reply("Error connecting to Discord: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -218,7 +281,7 @@ var cmdLogout = &commands.FullHandler{
|
|||||||
|
|
||||||
func fnLogout(ce *WrappedCommandEvent) {
|
func fnLogout(ce *WrappedCommandEvent) {
|
||||||
wasLoggedIn := ce.User.DiscordID != ""
|
wasLoggedIn := ce.User.DiscordID != ""
|
||||||
ce.User.Logout()
|
ce.User.Logout(false)
|
||||||
if wasLoggedIn {
|
if wasLoggedIn {
|
||||||
ce.Reply("Logged out successfully.")
|
ce.Reply("Logged out successfully.")
|
||||||
} else {
|
} else {
|
||||||
@@ -226,6 +289,29 @@ func fnLogout(ce *WrappedCommandEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cmdPing = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnPing),
|
||||||
|
Name: "ping",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Check your connection to Discord",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnPing(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Session == nil {
|
||||||
|
if ce.User.DiscordToken == "" {
|
||||||
|
ce.Reply("You're not logged in")
|
||||||
|
} else {
|
||||||
|
ce.Reply("You have a Discord token stored, but are not connected for some reason 🤔")
|
||||||
|
}
|
||||||
|
} else if ce.User.wasDisconnected {
|
||||||
|
ce.Reply("You're logged in, but the Discord connection seems to be dead 💥")
|
||||||
|
} else {
|
||||||
|
ce.Reply("You're logged in as %s#%s (`%s`)", ce.User.Session.State.User.Username, ce.User.Session.State.User.Discriminator, ce.User.DiscordID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var cmdDisconnect = &commands.FullHandler{
|
var cmdDisconnect = &commands.FullHandler{
|
||||||
Func: wrapCommand(fnDisconnect),
|
Func: wrapCommand(fnDisconnect),
|
||||||
Name: "disconnect",
|
Name: "disconnect",
|
||||||
@@ -271,7 +357,7 @@ var cmdRejoinSpace = &commands.FullHandler{
|
|||||||
Func: wrapCommand(fnRejoinSpace),
|
Func: wrapCommand(fnRejoinSpace),
|
||||||
Name: "rejoin-space",
|
Name: "rejoin-space",
|
||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionUnclassified,
|
Section: HelpSectionPortalManagement,
|
||||||
Description: "Ask the bridge for an invite to a space you left",
|
Description: "Ask the bridge for an invite to a space you left",
|
||||||
Args: "<_guild ID_/main/dms>",
|
Args: "<_guild ID_/main/dms>",
|
||||||
},
|
},
|
||||||
@@ -298,25 +384,182 @@ func fnRejoinSpace(ce *WrappedCommandEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var roomModerator = event.Type{Type: "fi.mau.discord.admin", Class: event.StateEventType}
|
||||||
|
|
||||||
|
var cmdSetRelay = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnSetRelay),
|
||||||
|
Name: "set-relay",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Create or set a relay webhook for a portal",
|
||||||
|
Args: "[room ID] <--url URL> OR <--create [name]>",
|
||||||
|
},
|
||||||
|
RequiresLogin: true,
|
||||||
|
RequiresEventLevel: roomModerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhookURLFormat = "https://discord.com/api/webhooks/%d/%s"
|
||||||
|
|
||||||
|
const selectRelayHelp = "Usage: `$cmdprefix [room ID] <--url URL> OR <--create [name]>`"
|
||||||
|
|
||||||
|
func fnSetRelay(ce *WrappedCommandEvent) {
|
||||||
|
portal := ce.Portal
|
||||||
|
if len(ce.Args) > 0 && strings.HasPrefix(ce.Args[0], "!") {
|
||||||
|
portal = ce.Bridge.GetPortalByMXID(id.RoomID(ce.Args[0]))
|
||||||
|
if portal == nil {
|
||||||
|
ce.Reply("Portal with room ID %s not found", ce.Args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ce.User.PermissionLevel < bridgeconfig.PermissionLevelAdmin {
|
||||||
|
levels, err := portal.MainIntent().PowerLevels(ce.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
ce.ZLog.Warn().Err(err).Msg("Failed to check room power levels")
|
||||||
|
ce.Reply("Failed to get room power levels to see if you're allowed to use that command")
|
||||||
|
return
|
||||||
|
} else if levels.GetUserLevel(ce.User.GetMXID()) < levels.GetEventLevel(roomModerator) {
|
||||||
|
ce.Reply("You don't have admin rights in that room")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ce.Args = ce.Args[1:]
|
||||||
|
} else if portal == nil {
|
||||||
|
ce.Reply("You must either run the command in a portal, or specify an internal room ID as the first parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log := ce.ZLog.With().Str("channel_id", portal.Key.ChannelID).Logger()
|
||||||
|
if portal.GuildID == "" {
|
||||||
|
ce.Reply("Only guild channels can have relays")
|
||||||
|
return
|
||||||
|
} else if portal.RelayWebhookID != "" {
|
||||||
|
webhookMeta, err := relayClient.WebhookWithToken(portal.RelayWebhookID, portal.RelayWebhookSecret)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to get existing webhook info")
|
||||||
|
ce.Reply("This channel has a relay webhook set, but getting its info failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ce.Reply("This channel already has a relay webhook %s (%s)", webhookMeta.Name, webhookMeta.ID)
|
||||||
|
return
|
||||||
|
} else if len(ce.Args) == 0 {
|
||||||
|
ce.Reply(selectRelayHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
createType := strings.ToLower(strings.TrimLeft(ce.Args[0], "-"))
|
||||||
|
var webhookMeta *discordgo.Webhook
|
||||||
|
switch createType {
|
||||||
|
case "url":
|
||||||
|
if len(ce.Args) < 2 {
|
||||||
|
ce.Reply("Usage: `$cmdprefix [room ID] --url <URL>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ce.Redact()
|
||||||
|
var webhookID int64
|
||||||
|
var webhookSecret string
|
||||||
|
_, err := fmt.Sscanf(ce.Args[1], webhookURLFormat, &webhookID, &webhookSecret)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Str("webhook_url", ce.Args[1]).Err(err).Msg("Failed to parse provided webhook URL")
|
||||||
|
ce.Reply("Invalid webhook URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
webhookMeta, err = relayClient.WebhookWithToken(strconv.FormatInt(webhookID, 10), webhookSecret)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to get webhook info")
|
||||||
|
ce.Reply("Failed to get webhook info: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "create":
|
||||||
|
perms, err := ce.User.Session.UserChannelPermissions(ce.User.DiscordID, portal.Key.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to check user permissions")
|
||||||
|
ce.Reply("Failed to check if you have permission to create webhooks")
|
||||||
|
return
|
||||||
|
} else if perms&discordgo.PermissionManageWebhooks == 0 {
|
||||||
|
log.Debug().Int64("perms", perms).Msg("User doesn't have permissions to manage webhooks in channel")
|
||||||
|
ce.Reply("You don't have permission to manage webhooks in that channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := "mautrix"
|
||||||
|
if len(ce.Args) > 1 {
|
||||||
|
name = strings.Join(ce.Args[1:], " ")
|
||||||
|
}
|
||||||
|
log.Debug().Str("webhook_name", name).Msg("Creating webhook")
|
||||||
|
webhookMeta, err = ce.User.Session.WebhookCreate(portal.Key.ChannelID, name, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to create webhook")
|
||||||
|
ce.Reply("Failed to create webhook: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ce.Reply(selectRelayHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if portal.Key.ChannelID != webhookMeta.ChannelID {
|
||||||
|
log.Debug().
|
||||||
|
Str("portal_channel_id", portal.Key.ChannelID).
|
||||||
|
Str("webhook_channel_id", webhookMeta.ChannelID).
|
||||||
|
Msg("Provided webhook is for wrong channel")
|
||||||
|
ce.Reply("That webhook is not for the right channel (expected %s, webhook is for %s)", portal.Key.ChannelID, webhookMeta.ChannelID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug().Str("webhook_id", webhookMeta.ID).Msg("Setting portal relay webhook")
|
||||||
|
portal.RelayWebhookID = webhookMeta.ID
|
||||||
|
portal.RelayWebhookSecret = webhookMeta.Token
|
||||||
|
portal.Update()
|
||||||
|
ce.Reply("Saved webhook %s (%s) as portal relay webhook", webhookMeta.Name, portal.RelayWebhookID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdUnsetRelay = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnUnsetRelay),
|
||||||
|
Name: "unset-relay",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Disable the relay webhook and optionally delete it on Discord",
|
||||||
|
Args: "[--delete]",
|
||||||
|
},
|
||||||
|
RequiresPortal: true,
|
||||||
|
RequiresEventLevel: roomModerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnUnsetRelay(ce *WrappedCommandEvent) {
|
||||||
|
if ce.Portal.RelayWebhookID == "" {
|
||||||
|
ce.Reply("This portal doesn't have a relay webhook")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ce.Args) > 0 && ce.Args[0] == "--delete" {
|
||||||
|
err := relayClient.WebhookDeleteWithToken(ce.Portal.RelayWebhookID, ce.Portal.RelayWebhookSecret)
|
||||||
|
if err != nil {
|
||||||
|
ce.Reply("Failed to delete webhook: %v", err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
ce.Reply("Successfully deleted webhook")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ce.Reply("Relay webhook disabled")
|
||||||
|
}
|
||||||
|
ce.Portal.RelayWebhookID = ""
|
||||||
|
ce.Portal.RelayWebhookSecret = ""
|
||||||
|
ce.Portal.Update()
|
||||||
|
}
|
||||||
|
|
||||||
var cmdGuilds = &commands.FullHandler{
|
var cmdGuilds = &commands.FullHandler{
|
||||||
Func: wrapCommand(fnGuilds),
|
Func: wrapCommand(fnGuilds),
|
||||||
Name: "guilds",
|
Name: "guilds",
|
||||||
Aliases: []string{"servers", "guild", "server"},
|
Aliases: []string{"servers", "guild", "server"},
|
||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionUnclassified,
|
Section: HelpSectionPortalManagement,
|
||||||
Description: "Guild bridging management",
|
Description: "Guild bridging management",
|
||||||
Args: "<status/bridge/unbridge> [_guild ID_] [--entire]",
|
Args: "<status/bridge/unbridge/bridging-mode> [_guild ID_] [...]",
|
||||||
},
|
},
|
||||||
RequiresLogin: true,
|
RequiresLogin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const smallGuildsHelp = "**Usage**: `$cmdprefix guilds <help/status/bridge/unbridge> [guild ID] [--entire]`"
|
const smallGuildsHelp = "**Usage**: `$cmdprefix guilds <help/status/bridge/unbridge> [guild ID] [...]`"
|
||||||
|
|
||||||
const fullGuildsHelp = smallGuildsHelp + `
|
const fullGuildsHelp = smallGuildsHelp + `
|
||||||
|
|
||||||
* **help** - View this help message.
|
* **help** - View this help message.
|
||||||
* **status** - View the list of guilds and their bridging status.
|
* **status** - View the list of guilds and their bridging status.
|
||||||
* **bridge <_guild ID_> [--entire]** - Enable bridging for a guild. The --entire flag auto-creates portals for all channels.
|
* **bridge <_guild ID_> [--entire]** - Enable bridging for a guild. The --entire flag auto-creates portals for all channels.
|
||||||
|
* **bridging-mode <_guild ID_> <_mode_>** - Set the mode for bridging messages and new channels in a guild.
|
||||||
* **unbridge <_guild ID_>** - Unbridge a guild and delete all channel portal rooms.`
|
* **unbridge <_guild ID_>** - Unbridge a guild and delete all channel portal rooms.`
|
||||||
|
|
||||||
func fnGuilds(ce *WrappedCommandEvent) {
|
func fnGuilds(ce *WrappedCommandEvent) {
|
||||||
@@ -333,6 +576,8 @@ func fnGuilds(ce *WrappedCommandEvent) {
|
|||||||
fnBridgeGuild(ce)
|
fnBridgeGuild(ce)
|
||||||
case "unbridge", "delete":
|
case "unbridge", "delete":
|
||||||
fnUnbridgeGuild(ce)
|
fnUnbridgeGuild(ce)
|
||||||
|
case "bridging-mode", "mode":
|
||||||
|
fnGuildBridgingMode(ce)
|
||||||
case "help":
|
case "help":
|
||||||
ce.Reply(fullGuildsHelp)
|
ce.Reply(fullGuildsHelp)
|
||||||
default:
|
default:
|
||||||
@@ -347,15 +592,11 @@ func fnListGuilds(ce *WrappedCommandEvent) {
|
|||||||
if guild == nil {
|
if guild == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
status := "not bridged"
|
|
||||||
if guild.MXID != "" {
|
|
||||||
status = "bridged"
|
|
||||||
}
|
|
||||||
var avatarHTML string
|
var avatarHTML string
|
||||||
if !guild.AvatarURL.IsEmpty() {
|
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))
|
items = append(items, fmt.Sprintf("<li>%s%s (<code>%s</code>) - %s</li>", avatarHTML, html.EscapeString(guild.Name), guild.ID, guild.BridgingMode.Description()))
|
||||||
}
|
}
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
ce.Reply("No guilds found")
|
ce.Reply("No guilds found")
|
||||||
@@ -384,11 +625,190 @@ func fnUnbridgeGuild(ce *WrappedCommandEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const availableModes = "Available modes:\n" +
|
||||||
|
"* `nothing` to never bridge any messages (default when unbridged)\n" +
|
||||||
|
"* `if-portal-exists` to bridge messages in existing portals, but drop messages in unbridged channels\n" +
|
||||||
|
"* `create-on-message` to bridge all messages and create portals if necessary on incoming messages (default after bridging)\n" +
|
||||||
|
"* `everything` to bridge all messages and create portals proactively on bridge startup (default if bridged with `--entire`)\n"
|
||||||
|
|
||||||
|
func fnGuildBridgingMode(ce *WrappedCommandEvent) {
|
||||||
|
if len(ce.Args) == 0 || len(ce.Args) > 2 {
|
||||||
|
ce.Reply("**Usage**: `$cmdprefix guilds bridging-mode <guild ID> [mode]`\n\n" + availableModes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guild := ce.Bridge.GetGuildByID(ce.Args[0], false)
|
||||||
|
if guild == nil {
|
||||||
|
ce.Reply("Guild not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ce.Args) == 1 {
|
||||||
|
ce.Reply("%s (%s) is currently set to %s (`%s`)\n\n%s", guild.PlainName, guild.ID, guild.BridgingMode.Description(), guild.BridgingMode.String(), availableModes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mode := database.ParseGuildBridgingMode(ce.Args[1])
|
||||||
|
if mode == database.GuildBridgeInvalid {
|
||||||
|
ce.Reply("Invalid guild bridging mode `%s`", ce.Args[1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guild.BridgingMode = mode
|
||||||
|
guild.Update()
|
||||||
|
ce.Reply("Set guild bridging mode to %s", mode.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdBridge = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnBridge),
|
||||||
|
Name: "bridge",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Bridge this room to a specific Discord channel",
|
||||||
|
Args: "[--replace[=delete]] <_channel ID_>",
|
||||||
|
},
|
||||||
|
RequiresEventLevel: roomModerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumber(str string) bool {
|
||||||
|
for _, chr := range str {
|
||||||
|
if chr < '0' || chr > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnBridge(ce *WrappedCommandEvent) {
|
||||||
|
if ce.Portal != nil {
|
||||||
|
ce.Reply("This is already a portal room. Unbridge with `$cmdprefix unbridge` first if you want to link it to a different channel.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var channelID string
|
||||||
|
var unbridgeOld, deleteOld bool
|
||||||
|
fail := true
|
||||||
|
for _, arg := range ce.Args {
|
||||||
|
arg = strings.ToLower(arg)
|
||||||
|
if arg == "--replace" {
|
||||||
|
unbridgeOld = true
|
||||||
|
} else if arg == "--replace=delete" {
|
||||||
|
unbridgeOld = true
|
||||||
|
deleteOld = true
|
||||||
|
} else if channelID == "" && isNumber(arg) {
|
||||||
|
channelID = arg
|
||||||
|
fail = false
|
||||||
|
} else {
|
||||||
|
fail = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fail {
|
||||||
|
ce.Reply("**Usage**: `$cmdprefix bridge [--replace[=delete]] <channel ID>`")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
portal := ce.User.GetExistingPortalByID(channelID)
|
||||||
|
if portal == nil {
|
||||||
|
ce.Reply("Channel not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
portal.roomCreateLock.Lock()
|
||||||
|
defer portal.roomCreateLock.Unlock()
|
||||||
|
if portal.MXID != "" {
|
||||||
|
hasUnbridgePermission := ce.User.PermissionLevel >= bridgeconfig.PermissionLevelAdmin
|
||||||
|
if !hasUnbridgePermission {
|
||||||
|
levels, err := portal.MainIntent().PowerLevels(portal.MXID)
|
||||||
|
if errors.Is(err, mautrix.MNotFound) {
|
||||||
|
ce.ZLog.Debug().Err(err).Msg("Got M_NOT_FOUND trying to get power levels to check if user can unbridge it, assuming the room is gone")
|
||||||
|
hasUnbridgePermission = true
|
||||||
|
} else if err != nil {
|
||||||
|
ce.ZLog.Warn().Err(err).Msg("Failed to check room power levels")
|
||||||
|
ce.Reply("Failed to get power levels in old room to see if you're allowed to unbridge it")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
hasUnbridgePermission = levels.GetUserLevel(ce.User.GetMXID()) >= levels.GetEventLevel(roomModerator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !unbridgeOld || !hasUnbridgePermission {
|
||||||
|
extraHelp := "Rerun the command with `--replace` or `--replace=delete` to unbridge the old room."
|
||||||
|
if !hasUnbridgePermission {
|
||||||
|
extraHelp = "Additionally, you do not have the permissions to unbridge the old room."
|
||||||
|
}
|
||||||
|
ce.Reply("That channel is already bridged to [%s](https://matrix.to/#/%s). %s", portal.Name, portal.MXID, extraHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ce.ZLog.Debug().
|
||||||
|
Str("old_room_id", portal.MXID.String()).
|
||||||
|
Bool("delete", deleteOld).
|
||||||
|
Msg("Unbridging old room")
|
||||||
|
portal.removeFromSpace()
|
||||||
|
portal.cleanup(!deleteOld)
|
||||||
|
portal.RemoveMXID()
|
||||||
|
ce.ZLog.Info().
|
||||||
|
Str("old_room_id", portal.MXID.String()).
|
||||||
|
Bool("delete", deleteOld).
|
||||||
|
Msg("Unbridged old room to make space for new bridge")
|
||||||
|
}
|
||||||
|
if portal.Guild != nil && portal.Guild.BridgingMode < database.GuildBridgeIfPortalExists {
|
||||||
|
ce.ZLog.Debug().Str("guild_id", portal.Guild.ID).Msg("Bumping bridging mode of portal guild to if-portal-exists")
|
||||||
|
portal.Guild.BridgingMode = database.GuildBridgeIfPortalExists
|
||||||
|
portal.Guild.Update()
|
||||||
|
}
|
||||||
|
ce.ZLog.Debug().Str("channel_id", portal.Key.ChannelID).Msg("Bridging room")
|
||||||
|
portal.MXID = ce.RoomID
|
||||||
|
portal.bridge.portalsLock.Lock()
|
||||||
|
portal.bridge.portalsByMXID[portal.MXID] = portal
|
||||||
|
portal.bridge.portalsLock.Unlock()
|
||||||
|
portal.updateRoomName()
|
||||||
|
portal.updateRoomAvatar()
|
||||||
|
portal.updateRoomTopic()
|
||||||
|
portal.updateSpace()
|
||||||
|
portal.UpdateBridgeInfo()
|
||||||
|
state, err := portal.MainIntent().State(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
ce.ZLog.Error().Err(err).Msg("Failed to update state cache for room")
|
||||||
|
} else {
|
||||||
|
encryptionEvent, isEncrypted := state[event.StateEncryption][""]
|
||||||
|
portal.Encrypted = isEncrypted && encryptionEvent.Content.AsEncryption().Algorithm == id.AlgorithmMegolmV1
|
||||||
|
}
|
||||||
|
portal.Update()
|
||||||
|
ce.Reply("Room successfully bridged")
|
||||||
|
ce.ZLog.Info().
|
||||||
|
Str("channel_id", portal.Key.ChannelID).
|
||||||
|
Bool("encrypted", portal.Encrypted).
|
||||||
|
Msg("Manual bridging complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdUnbridge = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnUnbridge),
|
||||||
|
Name: "unbridge",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Unbridge this room from the linked Discord channel",
|
||||||
|
},
|
||||||
|
RequiresPortal: true,
|
||||||
|
RequiresEventLevel: roomModerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDeletePortal = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnUnbridge),
|
||||||
|
Name: "delete-portal",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Unbridge this room and kick all Matrix users",
|
||||||
|
},
|
||||||
|
RequiresPortal: true,
|
||||||
|
RequiresEventLevel: roomModerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnUnbridge(ce *WrappedCommandEvent) {
|
||||||
|
ce.Portal.roomCreateLock.Lock()
|
||||||
|
defer ce.Portal.roomCreateLock.Unlock()
|
||||||
|
ce.Portal.removeFromSpace()
|
||||||
|
ce.Portal.cleanup(ce.Command == "unbridge")
|
||||||
|
ce.Portal.RemoveMXID()
|
||||||
|
}
|
||||||
|
|
||||||
var cmdDeleteAllPortals = &commands.FullHandler{
|
var cmdDeleteAllPortals = &commands.FullHandler{
|
||||||
Func: wrapCommand(fnDeleteAllPortals),
|
Func: wrapCommand(fnDeleteAllPortals),
|
||||||
Name: "delete-all-portals",
|
Name: "delete-all-portals",
|
||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionUnclassified,
|
Section: commands.HelpSectionAdmin,
|
||||||
Description: "Delete all portals.",
|
Description: "Delete all portals.",
|
||||||
},
|
},
|
||||||
RequiresAdmin: true,
|
RequiresAdmin: true,
|
||||||
@@ -396,14 +816,15 @@ var cmdDeleteAllPortals = &commands.FullHandler{
|
|||||||
|
|
||||||
func fnDeleteAllPortals(ce *WrappedCommandEvent) {
|
func fnDeleteAllPortals(ce *WrappedCommandEvent) {
|
||||||
portals := ce.Bridge.GetAllPortals()
|
portals := ce.Bridge.GetAllPortals()
|
||||||
if len(portals) == 0 {
|
guilds := ce.Bridge.GetAllGuilds()
|
||||||
|
if len(portals) == 0 && len(guilds) == 0 {
|
||||||
ce.Reply("Didn't find any portals")
|
ce.Reply("Didn't find any portals")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
leave := func(portal *Portal) {
|
leave := func(mxid id.RoomID, intent *appservice.IntentAPI) {
|
||||||
if len(portal.MXID) > 0 {
|
if len(mxid) > 0 {
|
||||||
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
|
_, _ = intent.KickUser(mxid, &mautrix.ReqKickUser{
|
||||||
Reason: "Deleting portal",
|
Reason: "Deleting portal",
|
||||||
UserID: ce.User.MXID,
|
UserID: ce.User.MXID,
|
||||||
})
|
})
|
||||||
@@ -412,19 +833,23 @@ func fnDeleteAllPortals(ce *WrappedCommandEvent) {
|
|||||||
customPuppet := ce.Bridge.GetPuppetByCustomMXID(ce.User.MXID)
|
customPuppet := ce.Bridge.GetPuppetByCustomMXID(ce.User.MXID)
|
||||||
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
||||||
intent := customPuppet.CustomIntent()
|
intent := customPuppet.CustomIntent()
|
||||||
leave = func(portal *Portal) {
|
leave = func(mxid id.RoomID, _ *appservice.IntentAPI) {
|
||||||
if len(portal.MXID) > 0 {
|
if len(mxid) > 0 {
|
||||||
_, _ = intent.LeaveRoom(portal.MXID)
|
_, _ = intent.LeaveRoom(mxid)
|
||||||
_, _ = intent.ForgetRoom(portal.MXID)
|
_, _ = intent.ForgetRoom(mxid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ce.Reply("Found %d portals, deleting...", len(portals))
|
ce.Reply("Found %d channel portals and %d guild portals, deleting...", len(portals), len(guilds))
|
||||||
for _, portal := range portals {
|
for _, portal := range portals {
|
||||||
portal.Delete()
|
portal.Delete()
|
||||||
leave(portal)
|
leave(portal.MXID, portal.MainIntent())
|
||||||
}
|
}
|
||||||
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background.")
|
for _, guild := range guilds {
|
||||||
|
guild.Delete()
|
||||||
|
leave(guild.MXID, ce.Bot)
|
||||||
|
}
|
||||||
|
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background. You'll have to restart the bridge or relogin before rooms can be bridged again.")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, portal := range portals {
|
for _, portal := range portals {
|
||||||
|
|||||||
@@ -28,12 +28,14 @@ import (
|
|||||||
"maunium.net/go/mautrix/bridge/commands"
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var HelpSectionDiscordBots = commands.HelpSection{Name: "Discord bot interaction", Order: 30}
|
||||||
|
|
||||||
var cmdCommands = &commands.FullHandler{
|
var cmdCommands = &commands.FullHandler{
|
||||||
Func: wrapCommand(fnCommands),
|
Func: wrapCommand(fnCommands),
|
||||||
Name: "commands",
|
Name: "commands",
|
||||||
Aliases: []string{"cmds", "cs"},
|
Aliases: []string{"cmds", "cs"},
|
||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionUnclassified,
|
Section: HelpSectionDiscordBots,
|
||||||
Description: "View parameters of bot interaction commands on Discord",
|
Description: "View parameters of bot interaction commands on Discord",
|
||||||
Args: "search <_query_> OR help <_command_>",
|
Args: "search <_query_> OR help <_command_>",
|
||||||
},
|
},
|
||||||
@@ -46,7 +48,7 @@ var cmdExec = &commands.FullHandler{
|
|||||||
Name: "exec",
|
Name: "exec",
|
||||||
Aliases: []string{"command", "cmd", "c", "exec", "e"},
|
Aliases: []string{"command", "cmd", "c", "exec", "e"},
|
||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionUnclassified,
|
Section: HelpSectionDiscordBots,
|
||||||
Description: "Run bot interaction commands on Discord",
|
Description: "Run bot interaction commands on Discord",
|
||||||
Args: "<_command_> [_arg=value ..._]",
|
Args: "<_command_> [_arg=value ..._]",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ type BridgeConfig struct {
|
|||||||
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
|
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
|
||||||
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
|
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
|
||||||
FederateRooms bool `yaml:"federate_rooms"`
|
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"`
|
DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"`
|
||||||
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import (
|
|||||||
func DoUpgrade(helper *up.Helper) {
|
func DoUpgrade(helper *up.Helper) {
|
||||||
bridgeconfig.Upgrader.DoUpgrade(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", "username_template")
|
||||||
helper.Copy(up.Str, "bridge", "displayname_template")
|
helper.Copy(up.Str, "bridge", "displayname_template")
|
||||||
helper.Copy(up.Str, "bridge", "channel_name_template")
|
helper.Copy(up.Str, "bridge", "channel_name_template")
|
||||||
@@ -45,6 +47,10 @@ func DoUpgrade(helper *up.Helper) {
|
|||||||
helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete")
|
helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete")
|
||||||
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
|
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
|
||||||
helper.Copy(up.Bool, "bridge", "federate_rooms")
|
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.Map, "bridge", "double_puppet_server_map")
|
||||||
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
||||||
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (br *DiscordBridge) newDoublePuppetClient(mxid id.UserID, accessToken strin
|
|||||||
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
|
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
|
||||||
if !found {
|
if !found {
|
||||||
if homeserver == br.AS.HomeserverDomain {
|
if homeserver == br.AS.HomeserverDomain {
|
||||||
homeserverURL = br.AS.HomeserverURL
|
homeserverURL = ""
|
||||||
} else if br.Config.Bridge.DoublePuppetAllowDiscovery {
|
} else if br.Config.Bridge.DoublePuppetAllowDiscovery {
|
||||||
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,16 +40,7 @@ func (br *DiscordBridge) newDoublePuppetClient(mxid id.UserID, accessToken strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := mautrix.NewClient(homeserverURL, mxid, accessToken)
|
return br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Logger = br.AS.Log.Sub(mxid.String())
|
|
||||||
client.Client = br.AS.HTTPClient
|
|
||||||
client.DefaultHTTPRetries = br.AS.DefaultHTTPRetries
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (puppet *Puppet) clearCustomMXID() {
|
func (puppet *Puppet) clearCustomMXID() {
|
||||||
@@ -111,13 +102,17 @@ func (puppet *Puppet) tryRelogin(cause error, action string) bool {
|
|||||||
if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
|
if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action)
|
log := puppet.log.With().
|
||||||
|
AnErr("cause_error", cause).
|
||||||
|
Str("while_action", action).
|
||||||
|
Logger()
|
||||||
|
log.Debug().Msg("Trying to relogin")
|
||||||
accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID)
|
accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err)
|
log.Error().Err(err).Msg("Failed to relogin")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action)
|
log.Info().Msg("Successfully relogined")
|
||||||
puppet.AccessToken = accessToken
|
puppet.AccessToken = accessToken
|
||||||
puppet.Update()
|
puppet.Update()
|
||||||
return true
|
return true
|
||||||
@@ -125,7 +120,7 @@ func (puppet *Puppet) tryRelogin(cause error, action string) bool {
|
|||||||
|
|
||||||
func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
|
func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
|
||||||
_, homeserver, _ := mxid.Parse()
|
_, homeserver, _ := mxid.Parse()
|
||||||
puppet.log.Debugfln("Logging into %s with shared secret", mxid)
|
puppet.log.Debug().Str("user_id", mxid.String()).Msg("Logging into double puppet target with shared secret")
|
||||||
loginSecret := puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]
|
loginSecret := puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]
|
||||||
client, err := puppet.bridge.newDoublePuppetClient(mxid, "")
|
client, err := puppet.bridge.newDoublePuppetClient(mxid, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ type Database struct {
|
|||||||
Message *MessageQuery
|
Message *MessageQuery
|
||||||
Thread *ThreadQuery
|
Thread *ThreadQuery
|
||||||
Reaction *ReactionQuery
|
Reaction *ReactionQuery
|
||||||
Emoji *EmojiQuery
|
|
||||||
Guild *GuildQuery
|
Guild *GuildQuery
|
||||||
Role *RoleQuery
|
Role *RoleQuery
|
||||||
File *FileQuery
|
File *FileQuery
|
||||||
@@ -54,10 +53,6 @@ func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
|
|||||||
db: db,
|
db: db,
|
||||||
log: log.Sub("Reaction"),
|
log: log.Sub("Reaction"),
|
||||||
}
|
}
|
||||||
db.Emoji = &EmojiQuery{
|
|
||||||
db: db,
|
|
||||||
log: log.Sub("Emoji"),
|
|
||||||
}
|
|
||||||
db.Guild = &GuildQuery{
|
db.Guild = &GuildQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: log.Sub("Guild"),
|
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
|
// language=postgresql
|
||||||
const (
|
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 = `
|
fileInsert = `
|
||||||
INSERT INTO discord_file (url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp)
|
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)
|
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))
|
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 {
|
type File struct {
|
||||||
db *Database
|
db *Database
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
URL string
|
URL string
|
||||||
Encrypted bool
|
Encrypted bool
|
||||||
|
MXC id.ContentURI
|
||||||
|
|
||||||
ID string
|
ID string
|
||||||
MXC id.ContentURI
|
EmojiName string
|
||||||
|
|
||||||
Size int
|
Size int
|
||||||
Width int
|
Width int
|
||||||
@@ -55,16 +61,15 @@ type File struct {
|
|||||||
MimeType string
|
MimeType string
|
||||||
|
|
||||||
DecryptionInfo *attachment.EncryptedFile
|
DecryptionInfo *attachment.EncryptedFile
|
||||||
|
Timestamp time.Time
|
||||||
Timestamp time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) Scan(row dbutil.Scannable) *File {
|
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 width, height sql.NullInt32
|
||||||
var timestamp int64
|
var timestamp int64
|
||||||
var mxc string
|
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 err != nil {
|
||||||
if !errors.Is(err, sql.ErrNoRows) {
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
f.log.Errorln("Database scan failed:", err)
|
f.log.Errorln("Database scan failed:", err)
|
||||||
@@ -73,6 +78,7 @@ func (f *File) Scan(row dbutil.Scannable) *File {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
f.ID = fileID.String
|
f.ID = fileID.String
|
||||||
|
f.EmojiName = emojiName.String
|
||||||
f.Timestamp = time.UnixMilli(timestamp)
|
f.Timestamp = time.UnixMilli(timestamp)
|
||||||
f.Width = int(width.Int32)
|
f.Width = int(width.Int32)
|
||||||
f.Height = int(height.Int32)
|
f.Height = int(height.Int32)
|
||||||
@@ -114,7 +120,7 @@ func (f *File) Insert(txn dbutil.Execable) {
|
|||||||
decryptionInfoStr.String = string(decryptionInfo)
|
decryptionInfoStr.String = string(decryptionInfo)
|
||||||
}
|
}
|
||||||
_, err := txn.Exec(fileInsert,
|
_, 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,
|
positiveIntToNullInt32(f.Width), positiveIntToNullInt32(f.Height), f.MimeType,
|
||||||
decryptionInfoStr, f.Timestamp.UnixMilli(),
|
decryptionInfoStr, f.Timestamp.UnixMilli(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package database
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
@@ -10,13 +12,76 @@ import (
|
|||||||
"maunium.net/go/mautrix/util/dbutil"
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GuildBridgingMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// GuildBridgeNothing tells the bridge to never bridge messages, not even checking if a portal exists.
|
||||||
|
GuildBridgeNothing GuildBridgingMode = iota
|
||||||
|
// GuildBridgeIfPortalExists tells the bridge to bridge messages in channels that already have portals.
|
||||||
|
GuildBridgeIfPortalExists
|
||||||
|
// GuildBridgeCreateOnMessage tells the bridge to create portals as soon as a message is received.
|
||||||
|
GuildBridgeCreateOnMessage
|
||||||
|
// GuildBridgeEverything tells the bridge to proactively create portals on startup and when receiving channel create notifications.
|
||||||
|
GuildBridgeEverything
|
||||||
|
|
||||||
|
GuildBridgeInvalid GuildBridgingMode = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseGuildBridgingMode(str string) GuildBridgingMode {
|
||||||
|
str = strings.ToLower(str)
|
||||||
|
str = strings.ReplaceAll(str, "-", "")
|
||||||
|
str = strings.ReplaceAll(str, "_", "")
|
||||||
|
switch str {
|
||||||
|
case "nothing", "0":
|
||||||
|
return GuildBridgeNothing
|
||||||
|
case "ifportalexists", "1":
|
||||||
|
return GuildBridgeIfPortalExists
|
||||||
|
case "createonmessage", "2":
|
||||||
|
return GuildBridgeCreateOnMessage
|
||||||
|
case "everything", "3":
|
||||||
|
return GuildBridgeEverything
|
||||||
|
default:
|
||||||
|
return GuildBridgeInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gbm GuildBridgingMode) String() string {
|
||||||
|
switch gbm {
|
||||||
|
case GuildBridgeNothing:
|
||||||
|
return "nothing"
|
||||||
|
case GuildBridgeIfPortalExists:
|
||||||
|
return "if-portal-exists"
|
||||||
|
case GuildBridgeCreateOnMessage:
|
||||||
|
return "create-on-message"
|
||||||
|
case GuildBridgeEverything:
|
||||||
|
return "everything"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gbm GuildBridgingMode) Description() string {
|
||||||
|
switch gbm {
|
||||||
|
case GuildBridgeNothing:
|
||||||
|
return "never bridge messages"
|
||||||
|
case GuildBridgeIfPortalExists:
|
||||||
|
return "bridge messages in existing portals"
|
||||||
|
case GuildBridgeCreateOnMessage:
|
||||||
|
return "bridge all messages and create portals on first message"
|
||||||
|
case GuildBridgeEverything:
|
||||||
|
return "bridge all messages and create portals proactively"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GuildQuery struct {
|
type GuildQuery struct {
|
||||||
db *Database
|
db *Database
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
guildSelect = "SELECT dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, auto_bridge_channels FROM guild"
|
guildSelect = "SELECT dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, bridging_mode FROM guild"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gq *GuildQuery) New() *Guild {
|
func (gq *GuildQuery) New() *Guild {
|
||||||
@@ -67,13 +132,13 @@ type Guild struct {
|
|||||||
AvatarURL id.ContentURI
|
AvatarURL id.ContentURI
|
||||||
AvatarSet bool
|
AvatarSet bool
|
||||||
|
|
||||||
AutoBridgeChannels bool
|
BridgingMode GuildBridgingMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Guild) Scan(row dbutil.Scannable) *Guild {
|
func (g *Guild) Scan(row dbutil.Scannable) *Guild {
|
||||||
var mxid sql.NullString
|
var mxid sql.NullString
|
||||||
var avatarURL string
|
var avatarURL string
|
||||||
err := row.Scan(&g.ID, &mxid, &g.PlainName, &g.Name, &g.NameSet, &g.Avatar, &avatarURL, &g.AvatarSet, &g.AutoBridgeChannels)
|
err := row.Scan(&g.ID, &mxid, &g.PlainName, &g.Name, &g.NameSet, &g.Avatar, &avatarURL, &g.AvatarSet, &g.BridgingMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, sql.ErrNoRows) {
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
g.log.Errorln("Database scan failed:", err)
|
g.log.Errorln("Database scan failed:", err)
|
||||||
@@ -82,6 +147,9 @@ func (g *Guild) Scan(row dbutil.Scannable) *Guild {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
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.MXID = id.RoomID(mxid.String)
|
||||||
g.AvatarURL, _ = id.ParseContentURI(avatarURL)
|
g.AvatarURL, _ = id.ParseContentURI(avatarURL)
|
||||||
return g
|
return g
|
||||||
@@ -96,10 +164,10 @@ func (g *Guild) mxidPtr() *id.RoomID {
|
|||||||
|
|
||||||
func (g *Guild) Insert() {
|
func (g *Guild) Insert() {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO guild (dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, auto_bridge_channels)
|
INSERT INTO guild (dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, bridging_mode)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
`
|
`
|
||||||
_, err := g.db.Exec(query, g.ID, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.AutoBridgeChannels)
|
_, err := g.db.Exec(query, g.ID, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.BridgingMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Warnfln("Failed to insert %s: %v", g.ID, err)
|
g.log.Warnfln("Failed to insert %s: %v", g.ID, err)
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -108,10 +176,10 @@ func (g *Guild) Insert() {
|
|||||||
|
|
||||||
func (g *Guild) Update() {
|
func (g *Guild) Update() {
|
||||||
query := `
|
query := `
|
||||||
UPDATE guild SET mxid=$1, plain_name=$2, name=$3, name_set=$4, avatar=$5, avatar_url=$6, avatar_set=$7, auto_bridge_channels=$8
|
UPDATE guild SET mxid=$1, plain_name=$2, name=$3, name_set=$4, avatar=$5, avatar_url=$6, avatar_set=$7, bridging_mode=$8
|
||||||
WHERE dcid=$9
|
WHERE dcid=$9
|
||||||
`
|
`
|
||||||
_, err := g.db.Exec(query, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.AutoBridgeChannels, g.ID)
|
_, err := g.db.Exec(query, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.BridgingMode, g.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Warnfln("Failed to update %s: %v", g.ID, err)
|
g.log.Warnfln("Failed to update %s: %v", g.ID, err)
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const (
|
|||||||
portalSelect = `
|
portalSelect = `
|
||||||
SELECT dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid,
|
SELECT dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid,
|
||||||
plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set,
|
plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set,
|
||||||
encrypted, in_space, first_event_id
|
encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret
|
||||||
FROM portal
|
FROM portal
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@@ -121,16 +121,19 @@ type Portal struct {
|
|||||||
InSpace id.RoomID
|
InSpace id.RoomID
|
||||||
|
|
||||||
FirstEventID id.EventID
|
FirstEventID id.EventID
|
||||||
|
|
||||||
|
RelayWebhookID string
|
||||||
|
RelayWebhookSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Portal) Scan(row dbutil.Scannable) *Portal {
|
func (p *Portal) Scan(row dbutil.Scannable) *Portal {
|
||||||
var otherUserID, guildID, parentID, mxid, firstEventID sql.NullString
|
var otherUserID, guildID, parentID, mxid, firstEventID, relayWebhookID, relayWebhookSecret sql.NullString
|
||||||
var chanType int32
|
var chanType int32
|
||||||
var avatarURL string
|
var avatarURL string
|
||||||
|
|
||||||
err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &chanType, &otherUserID, &guildID, &parentID,
|
err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &chanType, &otherUserID, &guildID, &parentID,
|
||||||
&mxid, &p.PlainName, &p.Name, &p.NameSet, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet,
|
&mxid, &p.PlainName, &p.Name, &p.NameSet, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet,
|
||||||
&p.Encrypted, &p.InSpace, &firstEventID)
|
&p.Encrypted, &p.InSpace, &firstEventID, &relayWebhookID, &relayWebhookSecret)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
@@ -148,6 +151,8 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal {
|
|||||||
p.Type = discordgo.ChannelType(chanType)
|
p.Type = discordgo.ChannelType(chanType)
|
||||||
p.FirstEventID = id.EventID(firstEventID.String)
|
p.FirstEventID = id.EventID(firstEventID.String)
|
||||||
p.AvatarURL, _ = id.ParseContentURI(avatarURL)
|
p.AvatarURL, _ = id.ParseContentURI(avatarURL)
|
||||||
|
p.RelayWebhookID = relayWebhookID.String
|
||||||
|
p.RelayWebhookSecret = relayWebhookSecret.String
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
@@ -156,13 +161,13 @@ func (p *Portal) Insert() {
|
|||||||
query := `
|
query := `
|
||||||
INSERT INTO portal (dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid,
|
INSERT INTO portal (dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid,
|
||||||
plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set,
|
plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set,
|
||||||
encrypted, in_space, first_event_id)
|
encrypted, in_space, first_event_id, relay_webhook_id, relay_webhook_secret)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
||||||
`
|
`
|
||||||
_, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.Type,
|
_, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.Type,
|
||||||
strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)),
|
strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)),
|
||||||
p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet,
|
p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet,
|
||||||
p.Encrypted, p.InSpace, p.FirstEventID.String())
|
p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Warnfln("Failed to insert %s: %v", p.Key, err)
|
p.log.Warnfln("Failed to insert %s: %v", p.Key, err)
|
||||||
@@ -175,13 +180,13 @@ func (p *Portal) Update() {
|
|||||||
UPDATE portal
|
UPDATE portal
|
||||||
SET type=$1, other_user_id=$2, dc_guild_id=$3, dc_parent_id=$4, mxid=$5,
|
SET type=$1, other_user_id=$2, dc_guild_id=$3, dc_parent_id=$4, mxid=$5,
|
||||||
plain_name=$6, name=$7, name_set=$8, topic=$9, topic_set=$10, avatar=$11, avatar_url=$12, avatar_set=$13,
|
plain_name=$6, name=$7, name_set=$8, topic=$9, topic_set=$10, avatar=$11, avatar_url=$12, avatar_set=$13,
|
||||||
encrypted=$14, in_space=$15, first_event_id=$16
|
encrypted=$14, in_space=$15, first_event_id=$16, relay_webhook_id=$17, relay_webhook_secret=$18
|
||||||
WHERE dcid=$17 AND receiver=$18
|
WHERE dcid=$19 AND receiver=$20
|
||||||
`
|
`
|
||||||
_, err := p.db.Exec(query,
|
_, err := p.db.Exec(query,
|
||||||
p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)),
|
p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)),
|
||||||
p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet,
|
p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet,
|
||||||
p.Encrypted, p.InSpace, p.FirstEventID.String(),
|
p.Encrypted, p.InSpace, p.FirstEventID.String(), strPtr(p.RelayWebhookID), strPtr(p.RelayWebhookSecret),
|
||||||
p.Key.ChannelID, p.Key.Receiver)
|
p.Key.ChannelID, p.Key.Receiver)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-- v0 -> v12: Latest revision
|
-- v0 -> v15: Latest revision
|
||||||
|
|
||||||
CREATE TABLE guild (
|
CREATE TABLE guild (
|
||||||
dcid TEXT PRIMARY KEY,
|
dcid TEXT PRIMARY KEY,
|
||||||
@@ -10,7 +10,7 @@ CREATE TABLE guild (
|
|||||||
avatar_url TEXT NOT NULL,
|
avatar_url TEXT NOT NULL,
|
||||||
avatar_set BOOLEAN NOT NULL,
|
avatar_set BOOLEAN NOT NULL,
|
||||||
|
|
||||||
auto_bridge_channels BOOLEAN NOT NULL
|
bridging_mode INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE portal (
|
CREATE TABLE portal (
|
||||||
@@ -39,6 +39,9 @@ CREATE TABLE portal (
|
|||||||
|
|
||||||
first_event_id TEXT NOT NULL,
|
first_event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
relay_webhook_id TEXT,
|
||||||
|
relay_webhook_secret TEXT,
|
||||||
|
|
||||||
PRIMARY KEY (dcid, receiver),
|
PRIMARY KEY (dcid, receiver),
|
||||||
CONSTRAINT portal_parent_fkey FOREIGN KEY (dc_parent_id, dc_parent_receiver) REFERENCES portal (dcid, receiver) ON DELETE CASCADE,
|
CONSTRAINT portal_parent_fkey FOREIGN KEY (dc_parent_id, dc_parent_receiver) REFERENCES portal (dcid, receiver) ON DELETE CASCADE,
|
||||||
CONSTRAINT portal_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild(dcid) ON DELETE CASCADE
|
CONSTRAINT portal_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild(dcid) ON DELETE CASCADE
|
||||||
@@ -126,12 +129,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
|
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 (
|
CREATE TABLE role (
|
||||||
dc_guild_id TEXT,
|
dc_guild_id TEXT,
|
||||||
dcid TEXT,
|
dcid TEXT,
|
||||||
@@ -154,18 +151,17 @@ CREATE TABLE role (
|
|||||||
CREATE TABLE discord_file (
|
CREATE TABLE discord_file (
|
||||||
url TEXT,
|
url TEXT,
|
||||||
encrypted BOOLEAN,
|
encrypted BOOLEAN,
|
||||||
|
mxc TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
id TEXT,
|
id TEXT,
|
||||||
mxc TEXT NOT NULL,
|
emoji_name TEXT,
|
||||||
|
|
||||||
size BIGINT NOT NULL,
|
|
||||||
width INTEGER,
|
|
||||||
height INTEGER,
|
|
||||||
mime_type TEXT NOT NULL,
|
|
||||||
|
|
||||||
|
size BIGINT NOT NULL,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
decryption_info jsonb,
|
decryption_info jsonb,
|
||||||
|
timestamp BIGINT NOT NULL,
|
||||||
timestamp BIGINT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (url, encrypted)
|
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;
|
||||||
7
database/upgrades/14-guild-bridging-mode.sql
Normal file
7
database/upgrades/14-guild-bridging-mode.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- v14: Add more modes of bridging guilds
|
||||||
|
ALTER TABLE guild ADD COLUMN bridging_mode INTEGER NOT NULL DEFAULT 0;
|
||||||
|
UPDATE guild SET bridging_mode=2 WHERE mxid<>'';
|
||||||
|
UPDATE guild SET bridging_mode=3 WHERE auto_bridge_channels=true;
|
||||||
|
ALTER TABLE guild DROP COLUMN auto_bridge_channels;
|
||||||
|
-- only: postgres
|
||||||
|
ALTER TABLE guild ALTER COLUMN bridging_mode DROP DEFAULT;
|
||||||
3
database/upgrades/15-portal-relay-webhook.sql
Normal file
3
database/upgrades/15-portal-relay-webhook.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- v15: Store relay webhook URL for portals
|
||||||
|
ALTER TABLE portal ADD COLUMN relay_webhook_id TEXT;
|
||||||
|
ALTER TABLE portal ADD COLUMN relay_webhook_secret TEXT;
|
||||||
19
discord.go
19
discord.go
@@ -18,30 +18,35 @@ func (user *User) channelIsBridgeable(channel *discordgo.Channel) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log := user.log.With().Str("guild_id", channel.GuildID).Str("channel_id", channel.ID).Logger()
|
||||||
|
|
||||||
member, err := user.Session.State.Member(channel.GuildID, user.DiscordID)
|
member, err := user.Session.State.Member(channel.GuildID, user.DiscordID)
|
||||||
if errors.Is(err, discordgo.ErrStateNotFound) {
|
if errors.Is(err, discordgo.ErrStateNotFound) {
|
||||||
user.log.Debugfln("Fetching own membership in %s to check own roles", channel.GuildID)
|
log.Debug().Msg("Fetching own membership in guild to check roles")
|
||||||
member, err = user.Session.GuildMember(channel.GuildID, user.DiscordID)
|
member, err = user.Session.GuildMember(channel.GuildID, user.DiscordID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to get own membership in %s from server to determine own roles for bridging %s: %v", channel.GuildID, channel.ID, err)
|
log.Warn().Err(err).Msg("Failed to get own membership in guild from server")
|
||||||
} else {
|
} else {
|
||||||
err = user.Session.State.MemberAdd(member)
|
err = user.Session.State.MemberAdd(member)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to add own membership in %s to cache: %v", channel.GuildID, err)
|
log.Warn().Err(err).Msg("Failed to add own membership in guild to cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
user.log.Warnfln("Failed to get own membership in %s from cache to determine own roles for bridging %s: %v", channel.GuildID, channel.ID, err)
|
log.Warn().Err(err).Msg("Failed to get own membership in guild from cache")
|
||||||
}
|
}
|
||||||
err = user.Session.State.ChannelAdd(channel)
|
err = user.Session.State.ChannelAdd(channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to add channel %s/%s to cache: %v", channel.GuildID, channel.ID, err)
|
log.Warn().Err(err).Msg("Failed to add channel to cache")
|
||||||
}
|
}
|
||||||
perms, err := user.Session.State.UserChannelPermissions(user.DiscordID, channel.ID)
|
perms, err := user.Session.State.UserChannelPermissions(user.DiscordID, channel.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to get permissions in %s/%s to determine if it's bridgeable: %v", channel.GuildID, channel.ID, err)
|
log.Warn().Err(err).Msg("Failed to get permissions in channel to determine if it's bridgeable")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
user.log.Debugfln("Computed permissions in %s/%s: %d (view channel: %t)", channel.GuildID, channel.ID, perms, perms&discordgo.PermissionViewChannel > 0)
|
log.Debug().
|
||||||
|
Int64("permissions", perms).
|
||||||
|
Bool("view_channel", perms&discordgo.PermissionViewChannel > 0).
|
||||||
|
Msg("Computed permissions in channel")
|
||||||
return perms&discordgo.PermissionViewChannel > 0
|
return perms&discordgo.PermissionViewChannel > 0
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
homeserver:
|
homeserver:
|
||||||
# The address that this appservice can use to connect to the homeserver.
|
# The address that this appservice can use to connect to the homeserver.
|
||||||
address: https://matrix.example.com
|
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).
|
# The domain of the homeserver (also known as server_name, used for MXIDs, etc).
|
||||||
domain: example.com
|
domain: example.com
|
||||||
|
|
||||||
@@ -140,6 +143,20 @@ bridge:
|
|||||||
# Whether or not created rooms should have federation enabled.
|
# Whether or not created rooms should have federation enabled.
|
||||||
# If false, created portal rooms will never be federated.
|
# If false, created portal rooms will never be federated.
|
||||||
federate_rooms: true
|
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
|
# Servers to always allow double puppeting from
|
||||||
double_puppet_server_map:
|
double_puppet_server_map:
|
||||||
example.com: https://example.com
|
example.com: https://example.com
|
||||||
@@ -240,12 +257,15 @@ bridge:
|
|||||||
"example.com": user
|
"example.com": user
|
||||||
"@admin:example.com": admin
|
"@admin:example.com": admin
|
||||||
|
|
||||||
|
# Logging config. See https://github.com/tulir/zeroconfig for details.
|
||||||
logging:
|
logging:
|
||||||
directory: ./logs
|
min_level: debug
|
||||||
file_name_format: '{{.Date}}-{{.Index}}.log'
|
writers:
|
||||||
file_date_format: "2006-01-02"
|
- type: stdout
|
||||||
file_mode: 384
|
format: pretty-colored
|
||||||
timestamp_format: Jan _2, 2006 15:04:05
|
- type: file
|
||||||
print_level: debug
|
format: json
|
||||||
print_json: false
|
filename: ./logs/mautrix-discord.log
|
||||||
file_json: false
|
max_size: 100
|
||||||
|
max_backups: 10
|
||||||
|
compress: true
|
||||||
|
|||||||
108
formatter.go
108
formatter.go
@@ -1,5 +1,5 @@
|
|||||||
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
// 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
|
// 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
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
@@ -21,8 +21,11 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
"github.com/yuin/goldmark/parser"
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
@@ -31,47 +34,88 @@ import (
|
|||||||
"maunium.net/go/mautrix/util/variationselector"
|
"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(`\\(__[^_]|\*\*[^*])`)
|
var escapeFixer = regexp.MustCompile(`\\(__[^_]|\*\*[^*])`)
|
||||||
|
|
||||||
func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string) string {
|
func escapeReplacement(s string) string {
|
||||||
text = escapeFixer.ReplaceAllStringFunc(text, func(s string) string {
|
return s[:2] + `\` + s[2:]
|
||||||
return s[:2] + `\` + s[2:]
|
}
|
||||||
})
|
|
||||||
|
|
||||||
mdRenderer := goldmark.New(
|
// indentableParagraphParser is the default paragraph parser with CanAcceptIndentedLine.
|
||||||
goldmark.WithParser(mdext.ParserWithoutFeatures(
|
// Used when disabling CodeBlockParser (as disabling it without a replacement will make indented blocks disappear).
|
||||||
parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
|
type indentableParagraphParser struct {
|
||||||
)),
|
parser.BlockParser
|
||||||
format.Extensions, format.HTMLOptions, discordExtensions,
|
}
|
||||||
goldmark.WithExtensions(&DiscordTag{portal}),
|
|
||||||
)
|
var defaultIndentableParagraphParser = &indentableParagraphParser{BlockParser: parser.NewParagraphParser()}
|
||||||
|
|
||||||
|
func (b *indentableParagraphParser) CanAcceptIndentedLine() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeFeaturesExceptLinks = []any{
|
||||||
|
parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
|
||||||
|
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
|
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 {
|
if err != nil {
|
||||||
panic(fmt.Errorf("markdown parser errored: %w", err))
|
panic(fmt.Errorf("markdown parser errored: %w", err))
|
||||||
}
|
}
|
||||||
return format.UnwrapSingleParagraph(buf.String())
|
return format.UnwrapSingleParagraph(buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatterContextUserKey = "fi.mau.discord.user"
|
|
||||||
const formatterContextPortalKey = "fi.mau.discord.portal"
|
const formatterContextPortalKey = "fi.mau.discord.portal"
|
||||||
|
const formatterContextAllowedMentionsKey = "fi.mau.discord.allowed_mentions"
|
||||||
|
|
||||||
func pillConverter(displayname, mxid, eventID string, ctx format.Context) string {
|
func appendIfNotContains(arr []string, newItem string) []string {
|
||||||
|
for _, item := range arr {
|
||||||
|
if item == newItem {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(arr, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) pillConverter(displayname, mxid, eventID string, ctx format.Context) string {
|
||||||
if len(mxid) == 0 {
|
if len(mxid) == 0 {
|
||||||
return displayname
|
return displayname
|
||||||
}
|
}
|
||||||
user := ctx.ReturnData[formatterContextUserKey].(*User)
|
|
||||||
if mxid[0] == '#' {
|
if mxid[0] == '#' {
|
||||||
alias, err := user.bridge.Bot.ResolveAlias(id.RoomAlias(mxid))
|
alias, err := br.Bot.ResolveAlias(id.RoomAlias(mxid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return displayname
|
return displayname
|
||||||
}
|
}
|
||||||
mxid = alias.RoomID.String()
|
mxid = alias.RoomID.String()
|
||||||
}
|
}
|
||||||
if mxid[0] == '!' {
|
if mxid[0] == '!' {
|
||||||
portal := user.bridge.GetPortalByMXID(id.RoomID(mxid))
|
portal := br.GetPortalByMXID(id.RoomID(mxid))
|
||||||
if portal != nil {
|
if portal != nil {
|
||||||
if eventID == "" {
|
if eventID == "" {
|
||||||
//currentPortal := ctx[formatterContextPortalKey].(*Portal)
|
//currentPortal := ctx[formatterContextPortalKey].(*Portal)
|
||||||
@@ -82,7 +126,7 @@ func pillConverter(displayname, mxid, eventID string, ctx format.Context) string
|
|||||||
//} else {
|
//} else {
|
||||||
// // TODO is mentioning private channels possible at all?
|
// // TODO is mentioning private channels possible at all?
|
||||||
//}
|
//}
|
||||||
} else if msg := user.bridge.DB.Message.GetByMXID(portal.Key, id.EventID(eventID)); msg != nil {
|
} else if msg := br.DB.Message.GetByMXID(portal.Key, id.EventID(eventID)); msg != nil {
|
||||||
guildID := portal.GuildID
|
guildID := portal.GuildID
|
||||||
if guildID == "" {
|
if guildID == "" {
|
||||||
guildID = "@me"
|
guildID = "@me"
|
||||||
@@ -91,12 +135,15 @@ func pillConverter(displayname, mxid, eventID string, ctx format.Context) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if mxid[0] == '@' {
|
} else if mxid[0] == '@' {
|
||||||
parsedID, ok := user.bridge.ParsePuppetMXID(id.UserID(mxid))
|
mentions := ctx.ReturnData[formatterContextAllowedMentionsKey].(*discordgo.MessageAllowedMentions)
|
||||||
|
parsedID, ok := br.ParsePuppetMXID(id.UserID(mxid))
|
||||||
if ok {
|
if ok {
|
||||||
|
mentions.Users = appendIfNotContains(mentions.Users, parsedID)
|
||||||
return fmt.Sprintf("<@%s>", parsedID)
|
return fmt.Sprintf("<@%s>", parsedID)
|
||||||
}
|
}
|
||||||
mentionedUser := user.bridge.GetUserByMXID(id.UserID(mxid))
|
mentionedUser := br.GetUserByMXID(id.UserID(mxid))
|
||||||
if mentionedUser != nil && mentionedUser.DiscordID != "" {
|
if mentionedUser != nil && mentionedUser.DiscordID != "" {
|
||||||
|
mentions.Users = appendIfNotContains(mentions.Users, mentionedUser.DiscordID)
|
||||||
return fmt.Sprintf("<@%s>", mentionedUser.DiscordID)
|
return fmt.Sprintf("<@%s>", mentionedUser.DiscordID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,17 +209,18 @@ var matrixHTMLParser = &format.HTMLParser{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) (string, *discordgo.MessageAllowedMentions) {
|
||||||
matrixHTMLParser.PillConverter = pillConverter
|
allowedMentions := &discordgo.MessageAllowedMentions{
|
||||||
}
|
Parse: []discordgo.AllowedMentionType{},
|
||||||
|
Users: []string{},
|
||||||
func (portal *Portal) parseMatrixHTML(user *User, content *event.MessageEventContent) string {
|
RepliedUser: true,
|
||||||
|
}
|
||||||
if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 {
|
if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 {
|
||||||
ctx := format.NewContext()
|
ctx := format.NewContext()
|
||||||
ctx.ReturnData[formatterContextUserKey] = user
|
|
||||||
ctx.ReturnData[formatterContextPortalKey] = portal
|
ctx.ReturnData[formatterContextPortalKey] = portal
|
||||||
return variationselector.Remove(matrixHTMLParser.Parse(content.FormattedBody, ctx))
|
ctx.ReturnData[formatterContextAllowedMentionsKey] = allowedMentions
|
||||||
|
return variationselector.FullyQualify(matrixHTMLParser.Parse(content.FormattedBody, ctx)), allowedMentions
|
||||||
} else {
|
} else {
|
||||||
return variationselector.Remove(escapeDiscordMarkdown(content.Body))
|
return variationselector.FullyQualify(escapeDiscordMarkdown(content.Body)), allowedMentions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,9 +96,11 @@ func (r *discordEveryoneHTMLRenderer) renderDiscordEveryone(w util.BufWriter, so
|
|||||||
return
|
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(
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||||
util.Prioritized(defaultDiscordEveryoneParser, 600),
|
util.Prioritized(defaultDiscordEveryoneParser, 600),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ import (
|
|||||||
"github.com/yuin/goldmark/renderer"
|
"github.com/yuin/goldmark/renderer"
|
||||||
"github.com/yuin/goldmark/text"
|
"github.com/yuin/goldmark/text"
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
"go.mau.fi/mautrix-discord/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
type astDiscordTag struct {
|
type astDiscordTag struct {
|
||||||
ast.BaseInline
|
ast.BaseInline
|
||||||
id int64
|
portal *Portal
|
||||||
|
id int64
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ ast.Node = (*astDiscordTag)(nil)
|
var _ ast.Node = (*astDiscordTag)(nil)
|
||||||
@@ -143,7 +143,10 @@ func (s *discordTagParser) Trigger() []byte {
|
|||||||
return []byte{'<'}
|
return []byte{'<'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var parserContextPortal = parser.NewContextKey()
|
||||||
|
|
||||||
func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||||
|
portal := pc.Get(parserContextPortal).(*Portal)
|
||||||
//before := block.PrecendingCharacter()
|
//before := block.PrecendingCharacter()
|
||||||
line, _ := block.PeekLine()
|
line, _ := block.PeekLine()
|
||||||
match := discordTagRegex.FindSubmatch(line)
|
match := discordTagRegex.FindSubmatch(line)
|
||||||
@@ -157,7 +160,7 @@ func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tag := astDiscordTag{id: id}
|
tag := astDiscordTag{id: id, portal: portal}
|
||||||
tagName := string(match[1])
|
tagName := string(match[1])
|
||||||
switch {
|
switch {
|
||||||
case tagName == "@":
|
case tagName == "@":
|
||||||
@@ -199,9 +202,9 @@ func (s *discordTagParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
|||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
type discordTagHTMLRenderer struct {
|
type discordTagHTMLRenderer struct{}
|
||||||
portal *Portal
|
|
||||||
}
|
var defaultDiscordTagHTMLRenderer = &discordTagHTMLRenderer{}
|
||||||
|
|
||||||
func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||||
reg.Register(astKindDiscordTag, r.renderDiscordMention)
|
reg.Register(astKindDiscordTag, r.renderDiscordMention)
|
||||||
@@ -259,17 +262,20 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
|||||||
}
|
}
|
||||||
switch node := n.(type) {
|
switch node := n.(type) {
|
||||||
case *astDiscordUserMention:
|
case *astDiscordUserMention:
|
||||||
puppet := r.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10))
|
if user := node.portal.bridge.GetUserByID(strconv.FormatInt(node.id, 10)); user != nil {
|
||||||
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Name)
|
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%[1]s">%[1]s</a>`, user.MXID)
|
||||||
|
} else if puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10)); puppet != nil {
|
||||||
|
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Name)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
case *astDiscordRoleMention:
|
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 {
|
if role != nil {
|
||||||
_, _ = fmt.Fprintf(w, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name)
|
_, _ = fmt.Fprintf(w, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case *astDiscordChannelMention:
|
case *astDiscordChannelMention:
|
||||||
portal := r.portal.bridge.GetExistingPortalByID(database.PortalKey{
|
portal := node.portal.bridge.GetExistingPortalByID(database.PortalKey{
|
||||||
ChannelID: strconv.FormatInt(node.id, 10),
|
ChannelID: strconv.FormatInt(node.id, 10),
|
||||||
Receiver: "",
|
Receiver: "",
|
||||||
})
|
})
|
||||||
@@ -282,7 +288,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case *astDiscordCustomEmoji:
|
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() {
|
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)
|
_, _ = fmt.Fprintf(w, `<img data-mx-emoticon src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name)
|
||||||
return
|
return
|
||||||
@@ -301,7 +307,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
|||||||
fullHumanReadable := ts.Format(discordTimestampStyle('F').Format())
|
fullHumanReadable := ts.Format(discordTimestampStyle('F').Format())
|
||||||
_, _ = fmt.Fprintf(w, `<time title="%s" datetime="%s"><strong>%s</strong></time>`, fullHumanReadable, fullRFC, formatted)
|
_, _ = fmt.Fprintf(w, `<time title="%s" datetime="%s"><strong>%s</strong></time>`, fullHumanReadable, fullRFC, formatted)
|
||||||
}
|
}
|
||||||
stringifiable, ok := n.(mautrix.Stringifiable)
|
stringifiable, ok := n.(fmt.Stringer)
|
||||||
if ok {
|
if ok {
|
||||||
_, _ = w.WriteString(stringifiable.String())
|
_, _ = w.WriteString(stringifiable.String())
|
||||||
} else {
|
} else {
|
||||||
@@ -310,15 +316,15 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscordTag struct {
|
type discordTag struct{}
|
||||||
Portal *Portal
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *DiscordTag) Extend(m goldmark.Markdown) {
|
var ExtDiscordTag = &discordTag{}
|
||||||
|
|
||||||
|
func (e *discordTag) Extend(m goldmark.Markdown) {
|
||||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||||
util.Prioritized(defaultDiscordTagParser, 600),
|
util.Prioritized(defaultDiscordTagParser, 600),
|
||||||
))
|
))
|
||||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
util.Prioritized(&discordTagHTMLRenderer{e.Portal}, 600),
|
util.Prioritized(defaultDiscordTagHTMLRenderer, 600),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
27
go.mod
27
go.mod
@@ -1,37 +1,40 @@
|
|||||||
module go.mau.fi/mautrix-discord
|
module go.mau.fi/mautrix-discord
|
||||||
|
|
||||||
go 1.18
|
go 1.19
|
||||||
|
|
||||||
require (
|
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/gabriel-vasile/mimetype v1.4.2
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/lib/pq v1.10.7
|
github.com/lib/pq v1.10.7
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
|
github.com/rs/zerolog v1.29.0
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.2
|
||||||
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/maulogger/v2 v2.4.1
|
||||||
maunium.net/go/mautrix v0.13.1-0.20230129131014-888cfabd8a52
|
maunium.net/go/mautrix v0.15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/zerolog v1.28.0 // indirect
|
|
||||||
github.com/tidwall/gjson v1.14.4 // indirect
|
github.com/tidwall/gjson v1.14.4 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
golang.org/x/crypto v0.5.0 // indirect
|
go.mau.fi/zeroconfig v0.1.2 // indirect
|
||||||
golang.org/x/net v0.5.0 // indirect
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
golang.org/x/sys v0.4.0 // indirect
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
maunium.net/go/mauflag v1.0.0 // indirect
|
maunium.net/go/mauflag v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230129125832-37978ff8e399
|
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20230301201402-cf4c62e5f53d
|
||||||
|
|||||||
50
go.sum
50
go.sum
@@ -1,12 +1,13 @@
|
|||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
github.com/beeper/discordgo v0.0.0-20230129125832-37978ff8e399 h1:3GZhhiyeXo/r40NmaQddBpCfosSSIrSrqZBLXJWrtYc=
|
github.com/beeper/discordgo v0.0.0-20230301201402-cf4c62e5f53d h1:xo6A9gSSu7mnxIXHBD1EPDyKEQFlI0N8r57Yf0gWiy8=
|
||||||
github.com/beeper/discordgo v0.0.0-20230129125832-37978ff8e399/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/beeper/discordgo v0.0.0-20230301201402-cf4c62e5f53d/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
@@ -27,8 +28,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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -36,8 +37,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
@@ -47,35 +48,34 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
|||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
|
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||||
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
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.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
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.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
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-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.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.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-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=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||||
maunium.net/go/mautrix v0.13.1-0.20230129131014-888cfabd8a52 h1:7KoJL/7eozYlu4GW2jADHO+Qhm8WL45Afcm7A45BivM=
|
maunium.net/go/mautrix v0.15.0 h1:gkK9HXc1SSPwY7qOAqchzj2xxYqiOYeee8lr28A2g/o=
|
||||||
maunium.net/go/mautrix v0.13.1-0.20230129131014-888cfabd8a52/go.mod h1:gYMQPsZ9lQpyKlVp+DGwOuc9LIcE/c8GZW2CvKHISgM=
|
maunium.net/go/mautrix v0.15.0/go.mod h1:1v8QVDd7q/eJ+eg4sgeOSEafBAFhkt4ab2i97M3IkNQ=
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ func (guild *Guild) CreateMatrixRoom(user *User, meta *discordgo.Guild) error {
|
|||||||
|
|
||||||
func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.Guild {
|
func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.Guild {
|
||||||
if meta.Unavailable {
|
if meta.Unavailable {
|
||||||
|
guild.log.Debugfln("Ignoring unavailable guild update")
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
changed := false
|
changed := false
|
||||||
@@ -312,6 +313,17 @@ func (guild *Guild) RemoveMXID() {
|
|||||||
guild.MXID = ""
|
guild.MXID = ""
|
||||||
guild.AvatarSet = false
|
guild.AvatarSet = false
|
||||||
guild.NameSet = false
|
guild.NameSet = false
|
||||||
guild.AutoBridgeChannels = false
|
guild.BridgingMode = database.GuildBridgeNothing
|
||||||
guild.Update()
|
guild.Update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (guild *Guild) Delete() {
|
||||||
|
guild.Guild.Delete()
|
||||||
|
guild.bridge.guildsLock.Lock()
|
||||||
|
delete(guild.bridge.guildsByID, guild.ID)
|
||||||
|
if guild.MXID != "" {
|
||||||
|
delete(guild.bridge.guildsByMXID, guild.MXID)
|
||||||
|
}
|
||||||
|
guild.bridge.guildsLock.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
26
main.go
26
main.go
@@ -18,11 +18,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/bridge"
|
"maunium.net/go/mautrix/bridge"
|
||||||
"maunium.net/go/mautrix/bridge/commands"
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util"
|
||||||
"maunium.net/go/mautrix/util/configupgrade"
|
"maunium.net/go/mautrix/util/configupgrade"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/config"
|
"go.mau.fi/mautrix-discord/config"
|
||||||
@@ -71,6 +77,8 @@ type DiscordBridge struct {
|
|||||||
puppets map[string]*Puppet
|
puppets map[string]*Puppet
|
||||||
puppetsByCustomMXID map[id.UserID]*Puppet
|
puppetsByCustomMXID map[id.UserID]*Puppet
|
||||||
puppetsLock sync.Mutex
|
puppetsLock sync.Mutex
|
||||||
|
|
||||||
|
attachmentTransfers *util.SyncMap[attachmentKey, *util.ReturnableOnce[*database.File]]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *DiscordBridge) GetExampleConfig() string {
|
func (br *DiscordBridge) GetExampleConfig() string {
|
||||||
@@ -89,8 +97,20 @@ func (br *DiscordBridge) Init() {
|
|||||||
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
|
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
|
||||||
br.RegisterCommands()
|
br.RegisterCommands()
|
||||||
|
|
||||||
|
matrixHTMLParser.PillConverter = br.pillConverter
|
||||||
|
|
||||||
br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database"))
|
br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database"))
|
||||||
discordLog = br.Log.Sub("Discord")
|
discordLog = br.ZLog.With().Str("component", "discordgo").Logger()
|
||||||
|
|
||||||
|
// TODO move this to mautrix-go?
|
||||||
|
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
|
||||||
|
files := strings.Split(file, "/")
|
||||||
|
file = files[len(files)-1]
|
||||||
|
name := runtime.FuncForPC(pc).Name()
|
||||||
|
fns := strings.Split(name, ".")
|
||||||
|
name = fns[len(fns)-1]
|
||||||
|
return fmt.Sprintf("%s:%d:%s()", file, line, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *DiscordBridge) Start() {
|
func (br *DiscordBridge) Start() {
|
||||||
@@ -163,12 +183,14 @@ func main() {
|
|||||||
|
|
||||||
puppets: make(map[string]*Puppet),
|
puppets: make(map[string]*Puppet),
|
||||||
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
||||||
|
|
||||||
|
attachmentTransfers: util.NewSyncMap[attachmentKey, *util.ReturnableOnce[*database.File]](),
|
||||||
}
|
}
|
||||||
br.Bridge = bridge.Bridge{
|
br.Bridge = bridge.Bridge{
|
||||||
Name: "mautrix-discord",
|
Name: "mautrix-discord",
|
||||||
URL: "https://github.com/mautrix/discord",
|
URL: "https://github.com/mautrix/discord",
|
||||||
Description: "A Matrix-Discord puppeting bridge.",
|
Description: "A Matrix-Discord puppeting bridge.",
|
||||||
Version: "0.1.0",
|
Version: "0.2.0",
|
||||||
ProtocolName: "Discord",
|
ProtocolName: "Discord",
|
||||||
|
|
||||||
CryptoPickleKey: "maunium.net/go/mautrix-whatsapp",
|
CryptoPickleKey: "maunium.net/go/mautrix-whatsapp",
|
||||||
|
|||||||
529
portal_convert.go
Normal file
529
portal_convert.go
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// 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
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConvertedMessage struct {
|
||||||
|
AttachmentID string
|
||||||
|
|
||||||
|
Type event.Type
|
||||||
|
Content *event.MessageEventContent
|
||||||
|
Extra map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) createMediaFailedMessage(bridgeErr error) *event.MessageEventContent {
|
||||||
|
return &event.MessageEventContent{
|
||||||
|
Body: fmt.Sprintf("Failed to bridge media: %v", bridgeErr),
|
||||||
|
MsgType: event.MsgNotice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DiscordStickerSize = 160
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent) *event.MessageEventContent {
|
||||||
|
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 {
|
||||||
|
portal.log.Errorfln("Error copying attachment %s to Matrix: %v", id, err)
|
||||||
|
return portal.createMediaFailedMessage(err)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
URL: dbFile.MXC.CUString(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.URL = dbFile.MXC.CUString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeName == "sticker" && (content.Info.Width > DiscordStickerSize || content.Info.Height > DiscordStickerSize) {
|
||||||
|
if content.Info.Width > content.Info.Height {
|
||||||
|
content.Info.Height /= content.Info.Width / DiscordStickerSize
|
||||||
|
content.Info.Width = DiscordStickerSize
|
||||||
|
} else if content.Info.Width < content.Info.Height {
|
||||||
|
content.Info.Width /= content.Info.Height / DiscordStickerSize
|
||||||
|
content.Info.Height = DiscordStickerSize
|
||||||
|
} else {
|
||||||
|
content.Info.Width = DiscordStickerSize
|
||||||
|
content.Info.Height = DiscordStickerSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordSticker(intent *appservice.IntentAPI, sticker *discordgo.Sticker) *ConvertedMessage {
|
||||||
|
var mime string
|
||||||
|
switch sticker.FormatType {
|
||||||
|
case discordgo.StickerFormatTypePNG:
|
||||||
|
mime = "image/png"
|
||||||
|
case discordgo.StickerFormatTypeAPNG:
|
||||||
|
mime = "image/apng"
|
||||||
|
case discordgo.StickerFormatTypeLottie:
|
||||||
|
mime = "application/json"
|
||||||
|
case discordgo.StickerFormatTypeGIF:
|
||||||
|
mime = "image/gif"
|
||||||
|
default:
|
||||||
|
portal.log.Warnfln("Unknown sticker format %d in %s", sticker.FormatType, sticker.ID)
|
||||||
|
}
|
||||||
|
return &ConvertedMessage{
|
||||||
|
AttachmentID: sticker.ID,
|
||||||
|
Type: event.EventSticker,
|
||||||
|
Content: portal.convertDiscordFile("sticker", intent, sticker.ID, sticker.URL(), &event.MessageEventContent{
|
||||||
|
Body: sticker.Name, // TODO find description from somewhere?
|
||||||
|
Info: &event.FileInfo{
|
||||||
|
MimeType: mime,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordAttachment(intent *appservice.IntentAPI, att *discordgo.MessageAttachment) *ConvertedMessage {
|
||||||
|
content := &event.MessageEventContent{
|
||||||
|
Body: att.Filename,
|
||||||
|
Info: &event.FileInfo{
|
||||||
|
Height: att.Height,
|
||||||
|
MimeType: att.ContentType,
|
||||||
|
Width: att.Width,
|
||||||
|
|
||||||
|
// This gets overwritten later after the file is uploaded to the homeserver
|
||||||
|
Size: att.Size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if att.Description != "" {
|
||||||
|
content.Body = att.Description
|
||||||
|
content.FileName = att.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(strings.Split(att.ContentType, "/")[0]) {
|
||||||
|
case "audio":
|
||||||
|
content.MsgType = event.MsgAudio
|
||||||
|
case "image":
|
||||||
|
content.MsgType = event.MsgImage
|
||||||
|
case "video":
|
||||||
|
content.MsgType = event.MsgVideo
|
||||||
|
default:
|
||||||
|
content.MsgType = event.MsgFile
|
||||||
|
}
|
||||||
|
content = portal.convertDiscordFile("attachment", intent, att.ID, att.URL, content)
|
||||||
|
return &ConvertedMessage{
|
||||||
|
AttachmentID: att.ID,
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordVideoEmbed(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *ConvertedMessage {
|
||||||
|
attachmentID := fmt.Sprintf("video_%s", embed.URL)
|
||||||
|
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, NoMeta)
|
||||||
|
if err != nil {
|
||||||
|
return &ConvertedMessage{
|
||||||
|
AttachmentID: attachmentID,
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: portal.createMediaFailedMessage(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := &event.MessageEventContent{
|
||||||
|
MsgType: event.MsgVideo,
|
||||||
|
Body: embed.URL,
|
||||||
|
Info: &event.FileInfo{
|
||||||
|
Width: embed.Video.Width,
|
||||||
|
Height: embed.Video.Height,
|
||||||
|
MimeType: dbFile.MimeType,
|
||||||
|
|
||||||
|
Size: dbFile.Size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if content.Info.Width == 0 && content.Info.Height == 0 {
|
||||||
|
content.Info.Width = dbFile.Width
|
||||||
|
content.Info.Height = dbFile.Height
|
||||||
|
}
|
||||||
|
if dbFile.DecryptionInfo != nil {
|
||||||
|
content.File = &event.EncryptedFileInfo{
|
||||||
|
EncryptedFile: *dbFile.DecryptionInfo,
|
||||||
|
URL: dbFile.MXC.CUString(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.URL = dbFile.MXC.CUString()
|
||||||
|
}
|
||||||
|
extra := map[string]any{}
|
||||||
|
if embed.Type == discordgo.EmbedTypeGifv {
|
||||||
|
extra["info"] = map[string]any{
|
||||||
|
"fi.mau.discord.gifv": true,
|
||||||
|
"fi.mau.loop": true,
|
||||||
|
"fi.mau.autoplay": true,
|
||||||
|
"fi.mau.hide_controls": true,
|
||||||
|
"fi.mau.no_audio": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ConvertedMessage{
|
||||||
|
AttachmentID: attachmentID,
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: content,
|
||||||
|
Extra: extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordMessage(intent *appservice.IntentAPI, msg *discordgo.Message) []*ConvertedMessage {
|
||||||
|
predictedLength := len(msg.Attachments) + len(msg.StickerItems)
|
||||||
|
if msg.Content != "" {
|
||||||
|
predictedLength++
|
||||||
|
}
|
||||||
|
parts := make([]*ConvertedMessage, 0, predictedLength)
|
||||||
|
if textPart := portal.convertDiscordTextMessage(intent, msg); textPart != nil {
|
||||||
|
parts = append(parts, textPart)
|
||||||
|
}
|
||||||
|
handledIDs := make(map[string]struct{})
|
||||||
|
for _, att := range msg.Attachments {
|
||||||
|
if _, handled := handledIDs[att.ID]; handled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handledIDs[att.ID] = struct{}{}
|
||||||
|
if part := portal.convertDiscordAttachment(intent, att); part != nil {
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sticker := range msg.StickerItems {
|
||||||
|
if _, handled := handledIDs[sticker.ID]; handled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handledIDs[sticker.ID] = struct{}{}
|
||||||
|
if part := portal.convertDiscordSticker(intent, sticker); part != nil {
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, embed := range msg.Embeds {
|
||||||
|
// Ignore non-video embeds, they're handled in convertDiscordTextMessage
|
||||||
|
if getEmbedType(embed) != EmbedVideo {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Discord deduplicates embeds by URL. It makes things easier for us too.
|
||||||
|
if _, handled := handledIDs[embed.URL]; handled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handledIDs[embed.URL] = struct{}{}
|
||||||
|
part := portal.convertDiscordVideoEmbed(intent, embed)
|
||||||
|
if part != nil {
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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>`
|
||||||
|
embedHTMLTitlePlain = `<p class="discord-embed-title"><strong>%s</strong></p>`
|
||||||
|
embedHTMLDescription = `<p class="discord-embed-description">%s</p>`
|
||||||
|
embedHTMLFieldName = `<th>%s</th>`
|
||||||
|
embedHTMLFieldValue = `<td>%s</td>`
|
||||||
|
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 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>`
|
||||||
|
embedFooterDateSeparator = ` • `
|
||||||
|
)
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed, msgID string, index int) string {
|
||||||
|
var htmlParts []string
|
||||||
|
if embed.Author != nil {
|
||||||
|
var authorHTML string
|
||||||
|
authorNameHTML := html.EscapeString(embed.Author.Name)
|
||||||
|
if embed.Author.URL != "" {
|
||||||
|
authorNameHTML = fmt.Sprintf(embedHTMLAuthorLink, embed.Author.URL, authorNameHTML)
|
||||||
|
}
|
||||||
|
authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML)
|
||||||
|
if embed.Author.ProxyIconURL != "" {
|
||||||
|
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 {
|
||||||
|
authorHTML = fmt.Sprintf(embedHTMLAuthorWithImage, dbFile.MXC, authorNameHTML)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
htmlParts = append(htmlParts, authorHTML)
|
||||||
|
}
|
||||||
|
if embed.Title != "" {
|
||||||
|
var titleHTML string
|
||||||
|
baseTitleHTML := portal.renderDiscordMarkdownOnlyHTML(embed.Title, false)
|
||||||
|
if embed.URL != "" {
|
||||||
|
titleHTML = fmt.Sprintf(embedHTMLTitleWithLink, html.EscapeString(embed.URL), baseTitleHTML)
|
||||||
|
} else {
|
||||||
|
titleHTML = fmt.Sprintf(embedHTMLTitlePlain, baseTitleHTML)
|
||||||
|
}
|
||||||
|
htmlParts = append(htmlParts, titleHTML)
|
||||||
|
}
|
||||||
|
if 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]
|
||||||
|
if portal.bridge.Config.Bridge.EmbedFieldsAsTables {
|
||||||
|
splitItems := []*discordgo.MessageEmbedField{item}
|
||||||
|
if item.Inline && len(embed.Fields) > i+1 && embed.Fields[i+1].Inline {
|
||||||
|
splitItems = append(splitItems, embed.Fields[i+1])
|
||||||
|
i++
|
||||||
|
if len(embed.Fields) > i+1 && embed.Fields[i+1].Inline {
|
||||||
|
splitItems = append(splitItems, embed.Fields[i+1])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerParts := make([]string, len(splitItems))
|
||||||
|
contentParts := make([]string, len(splitItems))
|
||||||
|
for j, splitItem := range splitItems {
|
||||||
|
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, false),
|
||||||
|
portal.renderDiscordMarkdownOnlyHTML(item.Value, true),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if embed.Image != nil {
|
||||||
|
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 {
|
||||||
|
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLImage, dbFile.MXC))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var embedDateHTML string
|
||||||
|
if embed.Timestamp != "" {
|
||||||
|
formattedTime := embed.Timestamp
|
||||||
|
parsedTS, err := time.Parse(time.RFC3339, embed.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Warnfln("Failed to parse timestamp in embed #%d of message %s: %v", index+1, msgID, err)
|
||||||
|
} else {
|
||||||
|
formattedTime = parsedTS.Format(discordTimestampStyle('F').Format())
|
||||||
|
}
|
||||||
|
embedDateHTML = fmt.Sprintf(embedHTMLDate, embed.Timestamp, formattedTime)
|
||||||
|
}
|
||||||
|
if embed.Footer != nil {
|
||||||
|
var footerHTML string
|
||||||
|
var datePart string
|
||||||
|
if embedDateHTML != "" {
|
||||||
|
datePart = embedFooterDateSeparator + embedDateHTML
|
||||||
|
}
|
||||||
|
footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart)
|
||||||
|
if embed.Footer.ProxyIconURL != "" {
|
||||||
|
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 {
|
||||||
|
footerHTML = fmt.Sprintf(embedHTMLFooterWithImage, dbFile.MXC, html.EscapeString(embed.Footer.Text), datePart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
htmlParts = append(htmlParts, footerHTML)
|
||||||
|
} else if embed.Timestamp != "" {
|
||||||
|
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLFooterOnlyDate, embedDateHTML))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(htmlParts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
compiledHTML := strings.Join(htmlParts, "")
|
||||||
|
if embed.Color != 0 {
|
||||||
|
compiledHTML = fmt.Sprintf(embedHTMLWrapperColor, embed.Color, compiledHTML)
|
||||||
|
} else {
|
||||||
|
compiledHTML = fmt.Sprintf(embedHTMLWrapper, compiledHTML)
|
||||||
|
}
|
||||||
|
return compiledHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
type BeeperLinkPreview struct {
|
||||||
|
mautrix.RespPreviewURL
|
||||||
|
MatchedURL string `json:"matched_url"`
|
||||||
|
ImageEncryption *event.EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordLinkEmbedImage(intent *appservice.IntentAPI, url string, width, height int, preview *BeeperLinkPreview) {
|
||||||
|
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 {
|
||||||
|
if width != 0 || height != 0 {
|
||||||
|
preview.ImageWidth = width
|
||||||
|
preview.ImageHeight = height
|
||||||
|
} else {
|
||||||
|
preview.ImageWidth = dbFile.Width
|
||||||
|
preview.ImageHeight = dbFile.Height
|
||||||
|
}
|
||||||
|
preview.ImageSize = dbFile.Size
|
||||||
|
preview.ImageType = dbFile.MimeType
|
||||||
|
if dbFile.Encrypted {
|
||||||
|
preview.ImageEncryption = &event.EncryptedFileInfo{
|
||||||
|
EncryptedFile: *dbFile.DecryptionInfo,
|
||||||
|
URL: dbFile.MXC.CUString(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preview.ImageURL = dbFile.MXC.CUString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordLinkEmbedToBeeper(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *BeeperLinkPreview {
|
||||||
|
var preview BeeperLinkPreview
|
||||||
|
preview.MatchedURL = embed.URL
|
||||||
|
preview.Title = embed.Title
|
||||||
|
preview.Description = embed.Description
|
||||||
|
if embed.Image != nil {
|
||||||
|
portal.convertDiscordLinkEmbedImage(intent, embed.Image.ProxyURL, embed.Image.Width, embed.Image.Height, &preview)
|
||||||
|
} else if embed.Thumbnail != nil {
|
||||||
|
portal.convertDiscordLinkEmbedImage(intent, embed.Thumbnail.ProxyURL, embed.Thumbnail.Width, embed.Thumbnail.Height, &preview)
|
||||||
|
}
|
||||||
|
return &preview
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgInteractionTemplateHTML = `<blockquote>
|
||||||
|
<a href="https://matrix.to/#/%s">%s</a> used <font color="#3771bb">/%s</font>
|
||||||
|
</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 isPlainGifMessage(msg *discordgo.Message) bool {
|
||||||
|
return len(msg.Embeds) == 1 && msg.Embeds[0].Video != nil && msg.Embeds[0].URL == msg.Content && msg.Embeds[0].Type == discordgo.EmbedTypeGifv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) convertDiscordTextMessage(intent *appservice.IntentAPI, msg *discordgo.Message) *ConvertedMessage {
|
||||||
|
if msg.Type == discordgo.MessageTypeCall {
|
||||||
|
return &ConvertedMessage{Type: event.EventMessage, Content: &event.MessageEventContent{
|
||||||
|
MsgType: event.MsgEmote,
|
||||||
|
Body: "started a call",
|
||||||
|
}}
|
||||||
|
} else if msg.Type == discordgo.MessageTypeGuildMemberJoin {
|
||||||
|
return &ConvertedMessage{Type: event.EventMessage, 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)
|
||||||
|
puppet.UpdateInfo(nil, msg.Interaction.User)
|
||||||
|
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, false))
|
||||||
|
}
|
||||||
|
previews := make([]*BeeperLinkPreview, 0)
|
||||||
|
for i, embed := range msg.Embeds {
|
||||||
|
switch getEmbedType(embed) {
|
||||||
|
case EmbedRich:
|
||||||
|
htmlParts = append(htmlParts, portal.convertDiscordRichEmbed(intent, embed, msg.ID, i))
|
||||||
|
case EmbedLinkPreview:
|
||||||
|
previews = append(previews, portal.convertDiscordLinkEmbedToBeeper(intent, embed))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg.Components) > 0 {
|
||||||
|
htmlParts = append(htmlParts, msgComponentTemplateHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(htmlParts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fullHTML := strings.Join(htmlParts, "\n")
|
||||||
|
if !msg.MentionEveryone {
|
||||||
|
fullHTML = strings.ReplaceAll(fullHTML, "@room", "@\u2063ro\u2063om")
|
||||||
|
}
|
||||||
|
|
||||||
|
content := format.HTMLToContent(fullHTML)
|
||||||
|
extraContent := map[string]any{
|
||||||
|
"com.beeper.linkpreviews": previews,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConvertedMessage{Type: event.EventMessage, Content: &content, Extra: extraContent}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-discord/database"
|
||||||
"go.mau.fi/mautrix-discord/remoteauth"
|
"go.mau.fi/mautrix-discord/remoteauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ func (p *ProvisioningAPI) logout(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
msg = "User wasn't logged in."
|
msg = "User wasn't logged in."
|
||||||
}
|
}
|
||||||
user.Logout()
|
user.Logout(false)
|
||||||
jsonResponse(w, http.StatusOK, Response{true, msg})
|
jsonResponse(w, http.StatusOK, Response{true, msg})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +328,6 @@ func (p *ProvisioningAPI) qrLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Infofln("Logged in as %s#%s (%s)", discordUser.Username, discordUser.Discriminator, discordUser.UserID)
|
log.Infofln("Logged in as %s#%s (%s)", discordUser.Username, discordUser.Discriminator, discordUser.UserID)
|
||||||
user.DiscordID = discordUser.UserID
|
|
||||||
user.Update()
|
|
||||||
|
|
||||||
if err = user.Login(discordUser.Token); err != nil {
|
if err = user.Login(discordUser.Token); err != nil {
|
||||||
log.Errorln("Failed to connect after logging in:", err)
|
log.Errorln("Failed to connect after logging in:", err)
|
||||||
@@ -429,11 +428,12 @@ func (p *ProvisioningAPI) reconnect(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type guildEntry struct {
|
type guildEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AvatarURL id.ContentURI `json:"avatar_url"`
|
AvatarURL id.ContentURI `json:"avatar_url"`
|
||||||
MXID id.RoomID `json:"mxid"`
|
MXID id.RoomID `json:"mxid"`
|
||||||
AutoBridge bool `json:"auto_bridge_channels"`
|
AutoBridge bool `json:"auto_bridge_channels"`
|
||||||
|
BridgingMode string `json:"bridging_mode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type respGuildsList struct {
|
type respGuildsList struct {
|
||||||
@@ -451,11 +451,12 @@ func (p *ProvisioningAPI) guildsList(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resp.Guilds = append(resp.Guilds, guildEntry{
|
resp.Guilds = append(resp.Guilds, guildEntry{
|
||||||
ID: guild.ID,
|
ID: guild.ID,
|
||||||
Name: guild.PlainName,
|
Name: guild.PlainName,
|
||||||
AvatarURL: guild.AvatarURL,
|
AvatarURL: guild.AvatarURL,
|
||||||
MXID: guild.MXID,
|
MXID: guild.MXID,
|
||||||
AutoBridge: guild.AutoBridgeChannels,
|
AutoBridge: guild.BridgingMode == database.GuildBridgeEverything,
|
||||||
|
BridgingMode: guild.BridgingMode.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +527,7 @@ func (p *ProvisioningAPI) guildsUnbridge(w http.ResponseWriter, r *http.Request)
|
|||||||
Error: "Guild not found",
|
Error: "Guild not found",
|
||||||
ErrCode: mautrix.MNotFound.ErrCode,
|
ErrCode: mautrix.MNotFound.ErrCode,
|
||||||
})
|
})
|
||||||
} else if !guild.AutoBridgeChannels && guild.MXID == "" {
|
} else if guild.BridgingMode == database.GuildBridgeNothing && guild.MXID == "" {
|
||||||
jsonResponse(w, http.StatusNotFound, Error{
|
jsonResponse(w, http.StatusNotFound, Error{
|
||||||
Error: "That guild is not bridged",
|
Error: "That guild is not bridged",
|
||||||
ErrCode: ErrCodeGuildNotBridged,
|
ErrCode: ErrCodeGuildNotBridged,
|
||||||
|
|||||||
19
puppet.go
19
puppet.go
@@ -5,9 +5,8 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
"maunium.net/go/mautrix/appservice"
|
||||||
"maunium.net/go/mautrix/bridge"
|
"maunium.net/go/mautrix/bridge"
|
||||||
@@ -20,7 +19,7 @@ type Puppet struct {
|
|||||||
*database.Puppet
|
*database.Puppet
|
||||||
|
|
||||||
bridge *DiscordBridge
|
bridge *DiscordBridge
|
||||||
log log.Logger
|
log zerolog.Logger
|
||||||
|
|
||||||
MXID id.UserID
|
MXID id.UserID
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ func (br *DiscordBridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
|
|||||||
return &Puppet{
|
return &Puppet{
|
||||||
Puppet: dbPuppet,
|
Puppet: dbPuppet,
|
||||||
bridge: br,
|
bridge: br,
|
||||||
log: br.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.ID)),
|
log: br.ZLog.With().Str("discord_user_id", dbPuppet.ID).Logger(),
|
||||||
|
|
||||||
MXID: br.FormatPuppetMXID(dbPuppet.ID),
|
MXID: br.FormatPuppetMXID(dbPuppet.ID),
|
||||||
}
|
}
|
||||||
@@ -202,7 +201,7 @@ func (puppet *Puppet) UpdateName(info *discordgo.User) bool {
|
|||||||
puppet.NameSet = false
|
puppet.NameSet = false
|
||||||
err := puppet.DefaultIntent().SetDisplayName(newName)
|
err := puppet.DefaultIntent().SetDisplayName(newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Warnln("Failed to update displayname:", err)
|
puppet.log.Warn().Err(err).Msg("Failed to update displayname")
|
||||||
} else {
|
} else {
|
||||||
go puppet.updatePortalMeta(func(portal *Portal) {
|
go puppet.updatePortalMeta(func(portal *Portal) {
|
||||||
if portal.UpdateNameDirect(puppet.Name) {
|
if portal.UpdateNameDirect(puppet.Name) {
|
||||||
@@ -228,7 +227,7 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
|
|||||||
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
|
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
|
||||||
url, err := uploadAvatar(puppet.DefaultIntent(), info.AvatarURL(""))
|
url, err := uploadAvatar(puppet.DefaultIntent(), info.AvatarURL(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Warnfln("Failed to reupload user avatar %s: %v", puppet.Avatar, err)
|
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
puppet.AvatarURL = url
|
puppet.AvatarURL = url
|
||||||
@@ -236,7 +235,7 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
|
|||||||
|
|
||||||
err := puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL)
|
err := puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Warnln("Failed to update avatar:", err)
|
puppet.log.Warn().Err(err).Msg("Failed to update avatar")
|
||||||
} else {
|
} else {
|
||||||
go puppet.updatePortalMeta(func(portal *Portal) {
|
go puppet.updatePortalMeta(func(portal *Portal) {
|
||||||
if portal.UpdateAvatarFromPuppet(puppet) {
|
if portal.UpdateAvatarFromPuppet(puppet) {
|
||||||
@@ -258,17 +257,17 @@ func (puppet *Puppet) UpdateInfo(source *User, info *discordgo.User) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
puppet.log.Debugfln("Fetching info through %s to update", source.DiscordID)
|
puppet.log.Debug().Str("source_user", source.DiscordID).Msg("Fetching info through user to update puppet")
|
||||||
info, err = source.Session.User(puppet.ID)
|
info, err = source.Session.User(puppet.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Errorfln("Failed to fetch info through %s: %v", source.DiscordID, err)
|
puppet.log.Error().Err(err).Str("source_user", source.DiscordID).Msg("Failed to fetch info through user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := puppet.DefaultIntent().EnsureRegistered()
|
err := puppet.DefaultIntent().EnsureRegistered()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Errorln("Failed to ensure registered:", err)
|
puppet.log.Error().Err(err).Msg("Failed to ensure registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := false
|
changed := false
|
||||||
|
|||||||
12
thread.go
12
thread.go
@@ -78,10 +78,16 @@ func (thread *Thread) Join(user *User) {
|
|||||||
if user.IsInPortal(thread.ID) {
|
if user.IsInPortal(thread.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.log.Debugfln("Joining thread %s@%s", thread.ID, thread.ParentID)
|
log := user.log.With().Str("thread_id", thread.ID).Str("channel_id", thread.ParentID).Logger()
|
||||||
err := user.Session.ThreadJoinWithLocation(thread.ID, discordgo.ThreadJoinLocationContextMenu)
|
log.Debug().Msg("Joining thread")
|
||||||
|
var err error
|
||||||
|
if user.Session.IsUser {
|
||||||
|
err = user.Session.ThreadJoinWithLocation(thread.ID, discordgo.ThreadJoinLocationContextMenu)
|
||||||
|
} else {
|
||||||
|
err = user.Session.ThreadJoin(thread.ID)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Error joining thread %s@%s: %v", thread.ID, thread.ParentID, err)
|
log.Error().Err(err).Msg("Error joining thread")
|
||||||
} else {
|
} else {
|
||||||
user.MarkInPortal(database.UserPortal{
|
user.MarkInPortal(database.UserPortal{
|
||||||
DiscordID: thread.ID,
|
DiscordID: thread.ID,
|
||||||
|
|||||||
407
user.go
407
user.go
@@ -6,17 +6,15 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/appservice"
|
"maunium.net/go/mautrix/appservice"
|
||||||
@@ -41,7 +39,7 @@ type User struct {
|
|||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
bridge *DiscordBridge
|
bridge *DiscordBridge
|
||||||
log log.Logger
|
log zerolog.Logger
|
||||||
|
|
||||||
PermissionLevel bridgeconfig.PermissionLevel
|
PermissionLevel bridgeconfig.PermissionLevel
|
||||||
|
|
||||||
@@ -76,34 +74,22 @@ func (user *User) GetRemoteName() string {
|
|||||||
return user.DiscordID
|
return user.DiscordID
|
||||||
}
|
}
|
||||||
|
|
||||||
var discordLog log.Logger
|
var discordLog zerolog.Logger
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
discordgo.Logger = func(msgL, caller int, format string, a ...interface{}) {
|
discordgo.Logger = func(msgL, caller int, format string, a ...interface{}) {
|
||||||
pc, file, line, _ := runtime.Caller(caller + 1)
|
var level zerolog.Level
|
||||||
|
|
||||||
files := strings.Split(file, "/")
|
|
||||||
file = files[len(files)-1]
|
|
||||||
|
|
||||||
name := runtime.FuncForPC(pc).Name()
|
|
||||||
fns := strings.Split(name, ".")
|
|
||||||
name = fns[len(fns)-1]
|
|
||||||
|
|
||||||
msg := fmt.Sprintf(format, a...)
|
|
||||||
|
|
||||||
var level log.Level
|
|
||||||
switch msgL {
|
switch msgL {
|
||||||
case discordgo.LogError:
|
case discordgo.LogError:
|
||||||
level = log.LevelError
|
level = zerolog.ErrorLevel
|
||||||
case discordgo.LogWarning:
|
case discordgo.LogWarning:
|
||||||
level = log.LevelWarn
|
level = zerolog.WarnLevel
|
||||||
case discordgo.LogInformational:
|
case discordgo.LogInformational:
|
||||||
level = log.LevelInfo
|
level = zerolog.InfoLevel
|
||||||
case discordgo.LogDebug:
|
case discordgo.LogDebug:
|
||||||
level = log.LevelDebug
|
level = zerolog.DebugLevel
|
||||||
}
|
}
|
||||||
|
discordLog.WithLevel(level).Caller(caller+1).Msgf(strings.TrimSpace(format), a...)
|
||||||
discordLog.Logfln(level, "%s:%d:%s() %s", file, line, name, msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +182,7 @@ func (br *DiscordBridge) NewUser(dbUser *database.User) *User {
|
|||||||
user := &User{
|
user := &User{
|
||||||
User: dbUser,
|
User: dbUser,
|
||||||
bridge: br,
|
bridge: br,
|
||||||
log: br.Log.Sub("User").Sub(string(dbUser.MXID)),
|
log: br.ZLog.With().Str("user_id", string(dbUser.MXID)).Logger(),
|
||||||
|
|
||||||
markedOpened: make(map[string]time.Time),
|
markedOpened: make(map[string]time.Time),
|
||||||
PermissionLevel: br.Config.Bridge.Permissions.Get(dbUser.MXID),
|
PermissionLevel: br.Config.Bridge.Permissions.Get(dbUser.MXID),
|
||||||
@@ -204,7 +190,7 @@ func (br *DiscordBridge) NewUser(dbUser *database.User) *User {
|
|||||||
pendingInteractions: make(map[string]*WrappedCommandEvent),
|
pendingInteractions: make(map[string]*WrappedCommandEvent),
|
||||||
}
|
}
|
||||||
user.nextDiscordUploadID.Store(rand.Int31n(100))
|
user.nextDiscordUploadID.Store(rand.Int31n(100))
|
||||||
user.BridgeState = br.NewBridgeStateQueue(user, user.log)
|
user.BridgeState = br.NewBridgeStateQueue(user)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +212,7 @@ func (br *DiscordBridge) getAllUsersWithToken() []*User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (br *DiscordBridge) startUsers() {
|
func (br *DiscordBridge) startUsers() {
|
||||||
br.Log.Debugln("Starting users")
|
br.ZLog.Debug().Msg("Starting users")
|
||||||
|
|
||||||
usersWithToken := br.getAllUsersWithToken()
|
usersWithToken := br.getAllUsersWithToken()
|
||||||
for _, u := range usersWithToken {
|
for _, u := range usersWithToken {
|
||||||
@@ -236,13 +222,13 @@ func (br *DiscordBridge) startUsers() {
|
|||||||
br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
|
br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
br.Log.Debugln("Starting custom puppets")
|
br.ZLog.Debug().Msg("Starting custom puppets")
|
||||||
for _, customPuppet := range br.GetAllPuppetsWithCustomMXID() {
|
for _, customPuppet := range br.GetAllPuppetsWithCustomMXID() {
|
||||||
go func(puppet *Puppet) {
|
go func(puppet *Puppet) {
|
||||||
br.Log.Debugln("Starting custom puppet", puppet.CustomMXID)
|
br.ZLog.Debug().Str("user_id", puppet.CustomMXID.String()).Msg("Starting custom puppet")
|
||||||
|
|
||||||
if err := puppet.StartCustomMXID(true); err != nil {
|
if err := puppet.StartCustomMXID(true); err != nil {
|
||||||
puppet.log.Errorln("Failed to start custom puppet:", err)
|
puppet.log.Error().Err(err).Msg("Failed to start custom puppet")
|
||||||
}
|
}
|
||||||
}(customPuppet)
|
}(customPuppet)
|
||||||
}
|
}
|
||||||
@@ -252,14 +238,14 @@ func (user *User) startupTryConnect(retryCount int) {
|
|||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
|
||||||
err := user.Connect()
|
err := user.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Error connecting: %v", err)
|
user.log.Error().Err(err).Msg("Error connecting on startup")
|
||||||
closeErr := &websocket.CloseError{}
|
closeErr := &websocket.CloseError{}
|
||||||
if errors.As(err, &closeErr) && closeErr.Code == 4004 {
|
if errors.As(err, &closeErr) && closeErr.Code == 4004 {
|
||||||
user.invalidAuthHandler(nil, nil)
|
user.invalidAuthHandler(nil, nil)
|
||||||
} else if retryCount < 6 {
|
} else if retryCount < 6 {
|
||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-unknown-websocket-error", Message: err.Error()})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-unknown-websocket-error", Message: err.Error()})
|
||||||
retryInSeconds := 2 << retryCount
|
retryInSeconds := 2 << retryCount
|
||||||
user.log.Debugfln("Retrying connection in %d seconds", retryInSeconds)
|
user.log.Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
|
||||||
time.Sleep(time.Duration(retryInSeconds) * time.Second)
|
time.Sleep(time.Duration(retryInSeconds) * time.Second)
|
||||||
user.startupTryConnect(retryCount + 1)
|
user.startupTryConnect(retryCount + 1)
|
||||||
} else {
|
} else {
|
||||||
@@ -333,7 +319,7 @@ func (user *User) getSpaceRoom(ptr *id.RoomID, name, topic string, parent id.Roo
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorln("Failed to auto-create space room:", err)
|
user.log.Error().Err(err).Msg("Failed to auto-create space room")
|
||||||
} else {
|
} else {
|
||||||
*ptr = resp.RoomID
|
*ptr = resp.RoomID
|
||||||
user.Update()
|
user.Update()
|
||||||
@@ -345,7 +331,10 @@ func (user *User) getSpaceRoom(ptr *id.RoomID, name, topic string, parent id.Roo
|
|||||||
Order: " 0000",
|
Order: " 0000",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Failed to add space room %s to parent space %s: %v", resp.RoomID, parent, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("created_space_id", resp.RoomID.String()).
|
||||||
|
Str("parent_space_id", parent.String()).
|
||||||
|
Msg("Failed to add created space room to parent space")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,34 +357,31 @@ func (user *User) tryAutomaticDoublePuppeting() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.log.Debugln("Checking if double puppeting needs to be enabled")
|
user.log.Debug().Msg("Checking if double puppeting needs to be enabled")
|
||||||
|
|
||||||
puppet := user.bridge.GetPuppetByID(user.DiscordID)
|
puppet := user.bridge.GetPuppetByID(user.DiscordID)
|
||||||
if puppet.CustomMXID != "" {
|
if puppet.CustomMXID != "" {
|
||||||
user.log.Debugln("User already has double-puppeting enabled")
|
user.log.Debug().Msg("User already has double-puppeting enabled")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, err := puppet.loginWithSharedSecret(user.MXID)
|
accessToken, err := puppet.loginWithSharedSecret(user.MXID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnln("Failed to login with shared secret:", err)
|
user.log.Warn().Err(err).Msg("Failed to login with shared secret")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = puppet.SwitchCustomMXID(accessToken, user.MXID)
|
err = puppet.SwitchCustomMXID(accessToken, user.MXID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err)
|
puppet.log.Warn().Err(err).Msg("Failed to switch to auto-logined custom puppet")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.log.Infoln("Successfully automatically enabled custom puppet")
|
user.log.Info().Msg("Successfully automatically enabled custom puppet")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) ViewingChannel(portal *Portal) bool {
|
func (user *User) ViewingChannel(portal *Portal) bool {
|
||||||
if portal.GuildID != "" {
|
if portal.GuildID != "" || !user.Session.IsUser {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
user.markedOpenedLock.Lock()
|
user.markedOpenedLock.Lock()
|
||||||
@@ -406,7 +392,9 @@ func (user *User) ViewingChannel(portal *Portal) bool {
|
|||||||
user.markedOpened[portal.Key.ChannelID] = time.Now()
|
user.markedOpened[portal.Key.ChannelID] = time.Now()
|
||||||
err := user.Session.MarkViewing(portal.Key.ChannelID)
|
err := user.Session.MarkViewing(portal.Key.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Failed to mark user as viewing %s: %v", portal.Key.ChannelID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("channel_id", portal.Key.ChannelID).
|
||||||
|
Msg("Failed to mark user as viewing channel")
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -419,16 +407,18 @@ func (user *User) mutePortal(intent *appservice.IntentAPI, portal *Portal, unmut
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if unmute {
|
if unmute {
|
||||||
user.log.Debugfln("Unmuting portal %s", portal.MXID)
|
user.log.Debug().Str("room_id", portal.MXID.String()).Msg("Unmuting portal")
|
||||||
err = intent.DeletePushRule("global", pushrules.RoomRule, string(portal.MXID))
|
err = intent.DeletePushRule("global", pushrules.RoomRule, string(portal.MXID))
|
||||||
} else {
|
} else {
|
||||||
user.log.Debugfln("Muting portal %s", portal.MXID)
|
user.log.Debug().Str("room_id", portal.MXID.String()).Msg("Muting portal")
|
||||||
err = intent.PutPushRule("global", pushrules.RoomRule, string(portal.MXID), &mautrix.ReqPutPushRule{
|
err = intent.PutPushRule("global", pushrules.RoomRule, string(portal.MXID), &mautrix.ReqPutPushRule{
|
||||||
Actions: []pushrules.PushActionType{pushrules.ActionDontNotify},
|
Actions: []pushrules.PushActionType{pushrules.ActionDontNotify},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
||||||
user.log.Warnfln("Failed to update push rule for %s through double puppet: %v", portal.MXID, err)
|
user.log.Warn().Err(err).
|
||||||
|
Str("room_id", portal.MXID.String()).
|
||||||
|
Msg("Failed to update push rule through double puppet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +430,7 @@ func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool)
|
|||||||
|
|
||||||
// TODO sync mute status properly
|
// TODO sync mute status properly
|
||||||
if portal.GuildID != "" && user.bridge.Config.Bridge.MuteChannelsOnCreate {
|
if portal.GuildID != "" && user.bridge.Config.Bridge.MuteChannelsOnCreate {
|
||||||
go user.mutePortal(doublePuppetIntent, portal, false)
|
user.mutePortal(doublePuppetIntent, portal, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,8 +444,31 @@ func (user *User) Login(token string) error {
|
|||||||
user.wasLoggedOut = false
|
user.wasLoggedOut = false
|
||||||
user.bridgeStateLock.Unlock()
|
user.bridgeStateLock.Unlock()
|
||||||
user.DiscordToken = token
|
user.DiscordToken = token
|
||||||
user.Update()
|
var err error
|
||||||
return user.Connect()
|
const maxRetries = 3
|
||||||
|
Loop:
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
err = user.Connect()
|
||||||
|
if err == nil {
|
||||||
|
user.Update()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
user.log.Error().Err(err).Msg("Error connecting for login")
|
||||||
|
closeErr := &websocket.CloseError{}
|
||||||
|
errors.As(err, &closeErr)
|
||||||
|
switch closeErr.Code {
|
||||||
|
case 4004, 4010, 4011, 4012, 4013, 4014:
|
||||||
|
break Loop
|
||||||
|
case 4000:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
if i < maxRetries-1 {
|
||||||
|
time.Sleep(time.Duration(i+1) * 2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.DiscordToken = ""
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) IsLoggedIn() bool {
|
func (user *User) IsLoggedIn() bool {
|
||||||
@@ -465,7 +478,7 @@ func (user *User) IsLoggedIn() bool {
|
|||||||
return user.DiscordToken != ""
|
return user.DiscordToken != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) Logout() {
|
func (user *User) Logout(isOverwriting bool) {
|
||||||
user.Lock()
|
user.Lock()
|
||||||
defer user.Unlock()
|
defer user.Unlock()
|
||||||
|
|
||||||
@@ -474,22 +487,30 @@ func (user *User) Logout() {
|
|||||||
if puppet.CustomMXID != "" {
|
if puppet.CustomMXID != "" {
|
||||||
err := puppet.SwitchCustomMXID("", "")
|
err := puppet.SwitchCustomMXID("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnln("Failed to logout-matrix while logging out of Discord:", err)
|
user.log.Warn().Err(err).Msg("Failed to disable custom puppet while logging out of Discord")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Session != nil {
|
if user.Session != nil {
|
||||||
if err := user.Session.Close(); err != nil {
|
if err := user.Session.Close(); err != nil {
|
||||||
user.log.Warnln("Error closing session:", err)
|
user.log.Warn().Err(err).Msg("Error closing session")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Session = nil
|
user.Session = nil
|
||||||
user.DiscordID = ""
|
|
||||||
user.DiscordToken = ""
|
user.DiscordToken = ""
|
||||||
user.ReadStateVersion = 0
|
user.ReadStateVersion = 0
|
||||||
|
if !isOverwriting {
|
||||||
|
user.bridge.usersLock.Lock()
|
||||||
|
if user.bridge.usersByID[user.DiscordID] == user {
|
||||||
|
delete(user.bridge.usersByID, user.DiscordID)
|
||||||
|
}
|
||||||
|
user.bridge.usersLock.Unlock()
|
||||||
|
}
|
||||||
|
user.DiscordID = ""
|
||||||
user.Update()
|
user.Update()
|
||||||
|
user.log.Info().Msg("User logged out")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) Connected() bool {
|
func (user *User) Connected() bool {
|
||||||
@@ -499,6 +520,24 @@ func (user *User) Connected() bool {
|
|||||||
return user.Session != nil
|
return user.Session != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BotIntents = discordgo.IntentGuilds |
|
||||||
|
discordgo.IntentGuildMessages |
|
||||||
|
discordgo.IntentGuildMessageReactions |
|
||||||
|
discordgo.IntentGuildMessageTyping |
|
||||||
|
discordgo.IntentGuildBans |
|
||||||
|
discordgo.IntentGuildEmojis |
|
||||||
|
discordgo.IntentGuildIntegrations |
|
||||||
|
discordgo.IntentGuildInvites |
|
||||||
|
//discordgo.IntentGuildVoiceStates |
|
||||||
|
//discordgo.IntentGuildScheduledEvents |
|
||||||
|
discordgo.IntentDirectMessages |
|
||||||
|
discordgo.IntentDirectMessageTyping |
|
||||||
|
discordgo.IntentDirectMessageTyping |
|
||||||
|
// Privileged intents
|
||||||
|
discordgo.IntentMessageContent |
|
||||||
|
//discordgo.IntentGuildPresences |
|
||||||
|
discordgo.IntentGuildMembers
|
||||||
|
|
||||||
func (user *User) Connect() error {
|
func (user *User) Connect() error {
|
||||||
user.Lock()
|
user.Lock()
|
||||||
defer user.Unlock()
|
defer user.Unlock()
|
||||||
@@ -507,7 +546,7 @@ func (user *User) Connect() error {
|
|||||||
return ErrNotLoggedIn
|
return ErrNotLoggedIn
|
||||||
}
|
}
|
||||||
|
|
||||||
user.log.Debugln("Connecting to discord")
|
user.log.Debug().Msg("Connecting to discord")
|
||||||
|
|
||||||
session, err := discordgo.New(user.DiscordToken)
|
session, err := discordgo.New(user.DiscordToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -517,10 +556,14 @@ func (user *User) Connect() error {
|
|||||||
if os.Getenv("DISCORD_DEBUG") == "1" {
|
if os.Getenv("DISCORD_DEBUG") == "1" {
|
||||||
session.LogLevel = discordgo.LogDebug
|
session.LogLevel = discordgo.LogDebug
|
||||||
}
|
}
|
||||||
|
if !session.IsUser {
|
||||||
|
session.Identify.Intents = BotIntents
|
||||||
|
}
|
||||||
|
|
||||||
user.Session = session
|
user.Session = session
|
||||||
|
|
||||||
user.Session.AddHandler(user.readyHandler)
|
user.Session.AddHandler(user.readyHandler)
|
||||||
|
user.Session.AddHandler(user.resumeHandler)
|
||||||
user.Session.AddHandler(user.connectedHandler)
|
user.Session.AddHandler(user.connectedHandler)
|
||||||
user.Session.AddHandler(user.disconnectedHandler)
|
user.Session.AddHandler(user.disconnectedHandler)
|
||||||
user.Session.AddHandler(user.invalidAuthHandler)
|
user.Session.AddHandler(user.invalidAuthHandler)
|
||||||
@@ -547,8 +590,6 @@ func (user *User) Connect() error {
|
|||||||
|
|
||||||
user.Session.AddHandler(user.interactionSuccessHandler)
|
user.Session.AddHandler(user.interactionSuccessHandler)
|
||||||
|
|
||||||
user.Session.Identify.Presence.Status = "online"
|
|
||||||
|
|
||||||
return user.Session.Open()
|
return user.Session.Open()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +600,7 @@ func (user *User) Disconnect() error {
|
|||||||
return ErrNotConnected
|
return ErrNotConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.log.Info().Msg("Disconnecting session manually")
|
||||||
if err := user.Session.Close(); err != nil {
|
if err := user.Session.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -566,22 +608,35 @@ func (user *User) Disconnect() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) bridgeMessage(guildID string) bool {
|
func (user *User) getGuildBridgingMode(guildID string) database.GuildBridgingMode {
|
||||||
if guildID == "" {
|
if guildID == "" {
|
||||||
return true
|
return database.GuildBridgeEverything
|
||||||
}
|
}
|
||||||
guild := user.bridge.GetGuildByID(guildID, false)
|
guild := user.bridge.GetGuildByID(guildID, false)
|
||||||
return guild != nil && guild.MXID != ""
|
if guild == nil {
|
||||||
|
return database.GuildBridgeNothing
|
||||||
|
}
|
||||||
|
return guild.BridgingMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
|
func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
|
||||||
user.log.Debugln("Discord connection ready")
|
user.log.Debug().Msg("Discord connection ready")
|
||||||
user.bridgeStateLock.Lock()
|
user.bridgeStateLock.Lock()
|
||||||
user.wasLoggedOut = false
|
user.wasLoggedOut = false
|
||||||
user.bridgeStateLock.Unlock()
|
user.bridgeStateLock.Unlock()
|
||||||
|
|
||||||
if user.DiscordID != r.User.ID {
|
if user.DiscordID != r.User.ID {
|
||||||
|
user.bridge.usersLock.Lock()
|
||||||
user.DiscordID = r.User.ID
|
user.DiscordID = r.User.ID
|
||||||
|
if previousUser, ok := user.bridge.usersByID[user.DiscordID]; ok && previousUser != user {
|
||||||
|
user.log.Warn().
|
||||||
|
Str("previous_user_id", previousUser.MXID.String()).
|
||||||
|
Msg("Another user is logged in with same Discord ID, logging them out")
|
||||||
|
// TODO send notice?
|
||||||
|
previousUser.Logout(true)
|
||||||
|
}
|
||||||
|
user.bridge.usersByID[user.DiscordID] = user
|
||||||
|
user.bridge.usersLock.Unlock()
|
||||||
user.Update()
|
user.Update()
|
||||||
}
|
}
|
||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBackfilling})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBackfilling})
|
||||||
@@ -601,7 +656,7 @@ func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
|
|||||||
}
|
}
|
||||||
user.PrunePortalList(updateTS)
|
user.PrunePortalList(updateTS)
|
||||||
|
|
||||||
if r.ReadState.Version > user.ReadStateVersion {
|
if r.ReadState != nil && r.ReadState.Version > user.ReadStateVersion {
|
||||||
// TODO can we figure out which read states are actually new?
|
// TODO can we figure out which read states are actually new?
|
||||||
for _, entry := range r.ReadState.Entries {
|
for _, entry := range r.ReadState.Entries {
|
||||||
user.messageAckHandler(nil, &discordgo.MessageAck{
|
user.messageAckHandler(nil, &discordgo.MessageAck{
|
||||||
@@ -613,9 +668,39 @@ func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
|
|||||||
user.Update()
|
user.Update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go user.subscribeGuilds(2 * time.Second)
|
||||||
|
|
||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) subscribeGuilds(delay time.Duration) {
|
||||||
|
if !user.Session.IsUser {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, guildMeta := range user.Session.State.Guilds {
|
||||||
|
guild := user.bridge.GetGuildByID(guildMeta.ID, false)
|
||||||
|
if guild != nil && guild.MXID != "" {
|
||||||
|
user.log.Debug().Str("guild_id", guild.ID).Msg("Subscribing to guild")
|
||||||
|
dat := discordgo.GuildSubscribeData{
|
||||||
|
GuildID: guild.ID,
|
||||||
|
Typing: true,
|
||||||
|
Activities: true,
|
||||||
|
Threads: true,
|
||||||
|
}
|
||||||
|
err := user.Session.SubscribeGuild(dat)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warn().Err(err).Str("guild_id", guild.ID).Msg("Failed to subscribe to guild")
|
||||||
|
}
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) resumeHandler(_ *discordgo.Session, r *discordgo.Resumed) {
|
||||||
|
user.log.Debug().Msg("Discord connection resumed")
|
||||||
|
user.subscribeGuilds(0 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
|
func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
|
||||||
if portal.MXID == "" {
|
if portal.MXID == "" {
|
||||||
return false
|
return false
|
||||||
@@ -624,7 +709,9 @@ func (user *User) addPrivateChannelToSpace(portal *Portal) bool {
|
|||||||
Via: []string{user.bridge.AS.HomeserverDomain},
|
Via: []string{user.bridge.AS.HomeserverDomain},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Failed to add DM room %s to user DM space: %v", portal.MXID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("room_id", portal.MXID.String()).
|
||||||
|
Msg("Failed to add DMM room to user DM space")
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
@@ -635,7 +722,9 @@ func (user *User) handlePrivateChannel(portal *Portal, meta *discordgo.Channel,
|
|||||||
if create && portal.MXID == "" {
|
if create && portal.MXID == "" {
|
||||||
err := portal.CreateMatrixRoom(user, meta)
|
err := portal.CreateMatrixRoom(user, meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Failed to create portal for private channel %s in initial sync: %v", portal.Key.ChannelID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("channel_id", portal.Key.ChannelID).
|
||||||
|
Msg("Failed to create portal for private channel in create handler")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
portal.UpdateInfo(user, meta)
|
portal.UpdateInfo(user, meta)
|
||||||
@@ -654,7 +743,9 @@ func (user *User) addGuildToSpace(guild *Guild, isInSpace bool, timestamp time.T
|
|||||||
Via: []string{user.bridge.AS.HomeserverDomain},
|
Via: []string{user.bridge.AS.HomeserverDomain},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Failed to add guild space %s to user space: %v", guild.MXID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("guild_space_id", guild.MXID.String()).
|
||||||
|
Msg("Failed to add guild space to user space")
|
||||||
} else {
|
} else {
|
||||||
isInSpace = true
|
isInSpace = true
|
||||||
}
|
}
|
||||||
@@ -697,7 +788,7 @@ func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) {
|
|||||||
}
|
}
|
||||||
txn, err := user.bridge.DB.Begin()
|
txn, err := user.bridge.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorln("Failed to start transaction for guild role sync:", err)
|
user.log.Error().Err(err).Msg("Failed to start transaction for guild role sync")
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
for _, role := range newRoles {
|
for _, role := range newRoles {
|
||||||
@@ -712,10 +803,10 @@ func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) {
|
|||||||
}
|
}
|
||||||
err = txn.Commit()
|
err = txn.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorln("Failed to commit guild role sync:", err)
|
user.log.Error().Err(err).Msg("Failed to commit guild role sync transaction")
|
||||||
rollbackErr := txn.Rollback()
|
rollbackErr := txn.Rollback()
|
||||||
if rollbackErr != nil {
|
if rollbackErr != nil {
|
||||||
user.log.Errorln("Failed to rollback errored guild role sync:", rollbackErr)
|
user.log.Error().Err(rollbackErr).Msg("Failed to rollback errored guild role sync transaction")
|
||||||
}
|
}
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -741,10 +832,13 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp
|
|||||||
if len(meta.Channels) > 0 {
|
if len(meta.Channels) > 0 {
|
||||||
for _, ch := range meta.Channels {
|
for _, ch := range meta.Channels {
|
||||||
portal := user.GetPortalByMeta(ch)
|
portal := user.GetPortalByMeta(ch)
|
||||||
if guild.AutoBridgeChannels && portal.MXID == "" && user.channelIsBridgeable(ch) {
|
if guild.BridgingMode >= database.GuildBridgeEverything && portal.MXID == "" && user.channelIsBridgeable(ch) {
|
||||||
err := portal.CreateMatrixRoom(user, ch)
|
err := portal.CreateMatrixRoom(user, ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Failed to create portal for guild channel %s/%s in initial sync: %v", guild.ID, ch.ID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("guild_id", guild.ID).
|
||||||
|
Str("channel_id", ch.ID).
|
||||||
|
Msg("Failed to create portal for guild channel in guild handler")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
portal.UpdateInfo(user, ch)
|
portal.UpdateInfo(user, ch)
|
||||||
@@ -760,7 +854,7 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp
|
|||||||
func (user *User) connectedHandler(_ *discordgo.Session, _ *discordgo.Connect) {
|
func (user *User) connectedHandler(_ *discordgo.Session, _ *discordgo.Connect) {
|
||||||
user.bridgeStateLock.Lock()
|
user.bridgeStateLock.Lock()
|
||||||
defer user.bridgeStateLock.Unlock()
|
defer user.bridgeStateLock.Unlock()
|
||||||
user.log.Debugln("Connected to Discord")
|
user.log.Debug().Msg("Connected to Discord")
|
||||||
if user.wasDisconnected {
|
if user.wasDisconnected {
|
||||||
user.wasDisconnected = false
|
user.wasDisconnected = false
|
||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||||
@@ -771,10 +865,10 @@ func (user *User) disconnectedHandler(_ *discordgo.Session, _ *discordgo.Disconn
|
|||||||
user.bridgeStateLock.Lock()
|
user.bridgeStateLock.Lock()
|
||||||
defer user.bridgeStateLock.Unlock()
|
defer user.bridgeStateLock.Unlock()
|
||||||
if user.wasLoggedOut {
|
if user.wasLoggedOut {
|
||||||
user.log.Debugln("Disconnected from Discord (not updating bridge state as user was just logged out)")
|
user.log.Debug().Msg("Disconnected from Discord (not updating bridge state as user was just logged out)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.log.Debugln("Disconnected from Discord")
|
user.log.Debug().Msg("Disconnected from Discord")
|
||||||
user.wasDisconnected = true
|
user.wasDisconnected = true
|
||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-transient-disconnect", Message: "Temporarily disconnected from Discord, trying to reconnect"})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "dc-transient-disconnect", Message: "Temporarily disconnected from Discord, trying to reconnect"})
|
||||||
}
|
}
|
||||||
@@ -782,44 +876,52 @@ func (user *User) disconnectedHandler(_ *discordgo.Session, _ *discordgo.Disconn
|
|||||||
func (user *User) invalidAuthHandler(_ *discordgo.Session, _ *discordgo.InvalidAuth) {
|
func (user *User) invalidAuthHandler(_ *discordgo.Session, _ *discordgo.InvalidAuth) {
|
||||||
user.bridgeStateLock.Lock()
|
user.bridgeStateLock.Lock()
|
||||||
defer user.bridgeStateLock.Unlock()
|
defer user.bridgeStateLock.Unlock()
|
||||||
user.log.Debugln("Got logged out from Discord")
|
user.log.Info().Msg("Got logged out from Discord due to invalid token")
|
||||||
user.wasLoggedOut = true
|
user.wasLoggedOut = true
|
||||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: "dc-websocket-disconnect-4004", Message: "Discord access token is no longer valid, please log in again"})
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: "dc-websocket-disconnect-4004", Message: "Discord access token is no longer valid, please log in again"})
|
||||||
go user.Logout()
|
go user.Logout(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) guildCreateHandler(_ *discordgo.Session, g *discordgo.GuildCreate) {
|
func (user *User) guildCreateHandler(_ *discordgo.Session, g *discordgo.GuildCreate) {
|
||||||
user.log.Infoln("Got guild create event for", g.ID)
|
user.log.Info().
|
||||||
|
Str("guild_id", g.ID).
|
||||||
|
Str("name", g.Name).
|
||||||
|
Bool("unavailable", g.Unavailable).
|
||||||
|
Msg("Got guild create event")
|
||||||
user.handleGuild(g.Guild, time.Now(), false)
|
user.handleGuild(g.Guild, time.Now(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) guildDeleteHandler(_ *discordgo.Session, g *discordgo.GuildDelete) {
|
func (user *User) guildDeleteHandler(_ *discordgo.Session, g *discordgo.GuildDelete) {
|
||||||
user.log.Infoln("Got guild delete event for", g.ID)
|
user.log.Info().Str("guild_id", g.ID).Msg("Got guild delete event")
|
||||||
user.MarkNotInPortal(g.ID)
|
user.MarkNotInPortal(g.ID)
|
||||||
guild := user.bridge.GetGuildByID(g.ID, false)
|
guild := user.bridge.GetGuildByID(g.ID, false)
|
||||||
if guild == nil || guild.MXID == "" {
|
if guild == nil || guild.MXID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user.bridge.Config.Bridge.DeleteGuildOnLeave && !user.PortalHasOtherUsers(g.ID) {
|
if user.bridge.Config.Bridge.DeleteGuildOnLeave && !user.PortalHasOtherUsers(g.ID) {
|
||||||
user.log.Debugfln("No other users in %s, cleaning up all portals", g.ID)
|
user.log.Debug().Str("guild_id", g.ID).Msg("No other users in guild, cleaning up all portals")
|
||||||
err := user.unbridgeGuild(g.ID)
|
err := user.unbridgeGuild(g.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to unbridge guild that was deleted: %v", err)
|
user.log.Warn().Err(err).Msg("Failed to unbridge guild that was deleted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) guildUpdateHandler(_ *discordgo.Session, g *discordgo.GuildUpdate) {
|
func (user *User) guildUpdateHandler(_ *discordgo.Session, g *discordgo.GuildUpdate) {
|
||||||
user.log.Debugln("Got guild update event for", g.ID)
|
user.log.Debug().Str("guild_id", g.ID).Msg("Got guild update event")
|
||||||
user.handleGuild(g.Guild, time.Now(), user.IsInSpace(g.ID))
|
user.handleGuild(g.Guild, time.Now(), user.IsInSpace(g.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.ChannelCreate) {
|
func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.ChannelCreate) {
|
||||||
if !user.bridgeMessage(c.GuildID) {
|
if user.getGuildBridgingMode(c.GuildID) < database.GuildBridgeEverything {
|
||||||
user.log.Debugfln("Ignoring channel create event in unbridged guild %s/%s", c.GuildID, c.ID)
|
user.log.Debug().
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Ignoring channel create event in unbridged guild")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.log.Infofln("Got channel create event for %s/%s", c.GuildID, c.ID)
|
user.log.Info().
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Got channel create event")
|
||||||
portal := user.GetPortalByMeta(c.Channel)
|
portal := user.GetPortalByMeta(c.Channel)
|
||||||
if portal.MXID != "" {
|
if portal.MXID != "" {
|
||||||
return
|
return
|
||||||
@@ -829,30 +931,40 @@ func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.Channe
|
|||||||
} else if user.channelIsBridgeable(c.Channel) {
|
} else if user.channelIsBridgeable(c.Channel) {
|
||||||
err := portal.CreateMatrixRoom(user, c.Channel)
|
err := portal.CreateMatrixRoom(user, c.Channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorfln("Error creating Matrix room for %s on channel create event: %v", c.ID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Error creating Matrix room after channel create event")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user.log.Debugfln("Got channel create event for %s, but it's not bridgeable, ignoring", c.ID)
|
user.log.Debug().
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Got channel create event, but it's not bridgeable, ignoring")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) channelDeleteHandler(_ *discordgo.Session, c *discordgo.ChannelDelete) {
|
func (user *User) channelDeleteHandler(_ *discordgo.Session, c *discordgo.ChannelDelete) {
|
||||||
portal := user.GetExistingPortalByID(c.ID)
|
portal := user.GetExistingPortalByID(c.ID)
|
||||||
if portal == nil {
|
if portal == nil {
|
||||||
user.log.Debugfln("Ignoring delete of unknown channel %s/%s", c.GuildID, c.ID)
|
user.log.Debug().
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Ignoring channel delete event of unknown channel")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.log.Infofln("Got channel delete event for %s/%s, cleaning up portal", c.GuildID, c.ID)
|
user.log.Info().
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Got channel delete event, cleaning up portal")
|
||||||
portal.Delete()
|
portal.Delete()
|
||||||
portal.cleanup(!user.bridge.Config.Bridge.DeletePortalOnChannelDelete)
|
portal.cleanup(!user.bridge.Config.Bridge.DeletePortalOnChannelDelete)
|
||||||
if c.GuildID == "" {
|
if c.GuildID == "" {
|
||||||
user.MarkNotInPortal(portal.Key.ChannelID)
|
user.MarkNotInPortal(portal.Key.ChannelID)
|
||||||
}
|
}
|
||||||
user.log.Debugfln("Completed cleaning up %s/%s", c.GuildID, c.ID)
|
user.log.Debug().
|
||||||
|
Str("guild_id", c.GuildID).Str("channel_id", c.ID).
|
||||||
|
Msg("Completed cleaning up channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) channelPinsUpdateHandler(_ *discordgo.Session, c *discordgo.ChannelPinsUpdate) {
|
func (user *User) channelPinsUpdateHandler(_ *discordgo.Session, c *discordgo.ChannelPinsUpdate) {
|
||||||
user.log.Debugln("channel pins update")
|
user.log.Debug().Msg("channel pins update")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) channelUpdateHandler(_ *discordgo.Session, c *discordgo.ChannelUpdate) {
|
func (user *User) channelUpdateHandler(_ *discordgo.Session, c *discordgo.ChannelUpdate) {
|
||||||
@@ -864,20 +976,61 @@ func (user *User) channelUpdateHandler(_ *discordgo.Session, c *discordgo.Channe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) findPortal(channelID string) (*Portal, *Thread) {
|
||||||
|
portal := user.GetExistingPortalByID(channelID)
|
||||||
|
if portal != nil {
|
||||||
|
return portal, nil
|
||||||
|
}
|
||||||
|
thread := user.bridge.GetThreadByID(channelID, nil)
|
||||||
|
if thread != nil && thread.Parent != nil {
|
||||||
|
return thread.Parent, thread
|
||||||
|
}
|
||||||
|
if !user.Session.IsUser {
|
||||||
|
channel, _ := user.Session.State.Channel(channelID)
|
||||||
|
if channel == nil {
|
||||||
|
user.log.Debug().Str("channel_id", channelID).Msg("Fetching info of unknown channel to handle message")
|
||||||
|
var err error
|
||||||
|
channel, err = user.Session.Channel(channelID)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warn().Err(err).Str("channel_id", channelID).Msg("Failed to get info of unknown channel")
|
||||||
|
} else {
|
||||||
|
user.log.Debug().Str("channel_id", channelID).Msg("Got info for channel to handle message")
|
||||||
|
_ = user.Session.State.ChannelAdd(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if channel != nil && user.channelIsBridgeable(channel) {
|
||||||
|
user.log.Debug().Str("channel_id", channelID).Msg("Creating portal and updating info to handle message")
|
||||||
|
portal = user.GetPortalByMeta(channel)
|
||||||
|
if channel.GuildID == "" {
|
||||||
|
user.handlePrivateChannel(portal, channel, time.Now(), false, false)
|
||||||
|
} else {
|
||||||
|
user.log.Warn().
|
||||||
|
Str("channel_id", channel.ID).Str("guild_id", channel.GuildID).
|
||||||
|
Msg("Unexpected unknown guild channel")
|
||||||
|
}
|
||||||
|
return portal, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildID string) {
|
func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildID string) {
|
||||||
if !user.bridgeMessage(guildID) {
|
if user.getGuildBridgingMode(guildID) <= database.GuildBridgeNothing {
|
||||||
|
// If guild bridging mode is nothing, don't even check if the portal exists
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
portal := user.GetExistingPortalByID(channelID)
|
portal, thread := user.findPortal(channelID)
|
||||||
var thread *Thread
|
|
||||||
if portal == nil {
|
if portal == nil {
|
||||||
thread = user.bridge.GetThreadByID(channelID, nil)
|
user.log.Debug().
|
||||||
if thread == nil || thread.Parent == nil {
|
Str("discord_event", typeName).
|
||||||
user.log.Debugfln("Dropping %s in unknown channel %s/%s", typeName, guildID, channelID)
|
Str("guild_id", guildID).
|
||||||
return
|
Str("channel_id", channelID).
|
||||||
}
|
Msg("Dropping event in unknown channel")
|
||||||
portal = thread.Parent
|
return
|
||||||
|
}
|
||||||
|
if mode := user.getGuildBridgingMode(portal.GuildID); mode <= database.GuildBridgeNothing || (portal.MXID == "" && mode <= database.GuildBridgeIfPortalExists) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.discordMessages <- portalDiscordMessage{
|
portal.discordMessages <- portalDiscordMessage{
|
||||||
@@ -942,14 +1095,20 @@ func (user *User) messageAckHandler(_ *discordgo.Session, m *discordgo.MessageAc
|
|||||||
}
|
}
|
||||||
msg := user.bridge.DB.Message.GetLastByDiscordID(portal.Key, m.MessageID)
|
msg := user.bridge.DB.Message.GetLastByDiscordID(portal.Key, m.MessageID)
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
user.log.Debugfln("Dropping message ack event for unknown message %s/%s", m.ChannelID, m.MessageID)
|
user.log.Debug().
|
||||||
|
Str("channel_id", m.ChannelID).Str("message_id", m.MessageID).
|
||||||
|
Msg("Dropping message ack event for unknown message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := dp.CustomIntent().SetReadMarkers(portal.MXID, user.makeReadMarkerContent(msg.MXID))
|
err := dp.CustomIntent().SetReadMarkers(portal.MXID, user.makeReadMarkerContent(msg.MXID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to mark %s/%s as read: %v", msg.MXID, msg.DiscordID, err)
|
user.log.Error().Err(err).
|
||||||
|
Str("event_id", msg.MXID.String()).Str("message_id", msg.DiscordID).
|
||||||
|
Msg("Failed to mark event as read")
|
||||||
} else {
|
} else {
|
||||||
user.log.Debugfln("Marked %s/%s as read after Discord message ack event", msg.MXID, msg.DiscordID)
|
user.log.Debug().
|
||||||
|
Str("event_id", msg.MXID.String()).Str("message_id", msg.DiscordID).
|
||||||
|
Msg("Marked event as read after Discord message ack")
|
||||||
if user.ReadStateVersion < m.Version {
|
if user.ReadStateVersion < m.Version {
|
||||||
user.ReadStateVersion = m.Version
|
user.ReadStateVersion = m.Version
|
||||||
// TODO maybe don't update every time?
|
// TODO maybe don't update every time?
|
||||||
@@ -963,11 +1122,7 @@ func (user *User) typingStartHandler(_ *discordgo.Session, t *discordgo.TypingSt
|
|||||||
if portal == nil || portal.MXID == "" {
|
if portal == nil || portal.MXID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
puppet := user.bridge.GetPuppetByID(t.UserID)
|
portal.handleDiscordTyping(t)
|
||||||
_, 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) interactionSuccessHandler(_ *discordgo.Session, s *discordgo.InteractionSuccess) {
|
func (user *User) interactionSuccessHandler(_ *discordgo.Session, s *discordgo.InteractionSuccess) {
|
||||||
@@ -975,9 +1130,9 @@ func (user *User) interactionSuccessHandler(_ *discordgo.Session, s *discordgo.I
|
|||||||
defer user.pendingInteractionsLock.Unlock()
|
defer user.pendingInteractionsLock.Unlock()
|
||||||
ce, ok := user.pendingInteractions[s.Nonce]
|
ce, ok := user.pendingInteractions[s.Nonce]
|
||||||
if !ok {
|
if !ok {
|
||||||
user.log.Debugfln("Got interaction success for unknown interaction %s/%s", s.Nonce, s.ID)
|
user.log.Debug().Str("nonce", s.Nonce).Str("id", s.ID).Msg("Got interaction success for unknown interaction")
|
||||||
} else {
|
} else {
|
||||||
user.log.Infofln("Got interaction success for pending interaction %s/%s", s.Nonce, s.ID)
|
user.log.Debug().Str("nonce", s.Nonce).Str("id", s.ID).Msg("Got interaction success for pending interaction")
|
||||||
ce.React("✅")
|
ce.React("✅")
|
||||||
delete(user.pendingInteractions, s.Nonce)
|
delete(user.pendingInteractions, s.Nonce)
|
||||||
}
|
}
|
||||||
@@ -1009,7 +1164,7 @@ func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID,
|
|||||||
user.bridge.StateStore.SetMembership(roomID, user.MXID, event.MembershipJoin)
|
user.bridge.StateStore.SetMembership(roomID, user.MXID, event.MembershipJoin)
|
||||||
ret = true
|
ret = true
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
user.log.Warnfln("Failed to invite user to %s: %v", roomID, err)
|
user.log.Error().Err(err).Str("room_id", roomID.String()).Msg("Failed to invite user to room")
|
||||||
} else {
|
} else {
|
||||||
ret = true
|
ret = true
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1172,7 @@ func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID,
|
|||||||
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
||||||
err = customPuppet.CustomIntent().EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
|
err = customPuppet.CustomIntent().EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Failed to auto-join %s: %v", roomID, err)
|
user.log.Warn().Err(err).Str("room_id", roomID.String()).Msg("Failed to auto-join room")
|
||||||
ret = false
|
ret = false
|
||||||
} else {
|
} else {
|
||||||
ret = true
|
ret = true
|
||||||
@@ -1063,7 +1218,7 @@ func (user *User) updateDirectChats(chats map[id.UserID][]id.RoomID) {
|
|||||||
method = http.MethodPut
|
method = http.MethodPut
|
||||||
}
|
}
|
||||||
|
|
||||||
user.log.Debugln("Updating m.direct list on homeserver")
|
user.log.Debug().Msg("Updating m.direct list on homeserver")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if user.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareAsmux {
|
if user.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareAsmux {
|
||||||
@@ -1079,8 +1234,7 @@ func (user *User) updateDirectChats(chats map[id.UserID][]id.RoomID) {
|
|||||||
|
|
||||||
err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats)
|
err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnln("Failed to get m.direct list to update it:", err)
|
user.log.Warn().Err(err).Msg("Failed to get m.direct event to update it")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1098,7 +1252,7 @@ func (user *User) updateDirectChats(chats map[id.UserID][]id.RoomID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnln("Failed to update m.direct list:", err)
|
user.log.Warn().Err(err).Msg("Failed to update m.direct event")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1112,19 +1266,36 @@ func (user *User) bridgeGuild(guildID string, everything bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log := user.log.With().Str("guild_id", guild.ID).Logger()
|
||||||
user.addGuildToSpace(guild, false, time.Now())
|
user.addGuildToSpace(guild, false, time.Now())
|
||||||
for _, ch := range meta.Channels {
|
for _, ch := range meta.Channels {
|
||||||
portal := user.GetPortalByMeta(ch)
|
portal := user.GetPortalByMeta(ch)
|
||||||
if (everything && user.channelIsBridgeable(ch)) || ch.Type == discordgo.ChannelTypeGuildCategory {
|
if (everything && user.channelIsBridgeable(ch)) || ch.Type == discordgo.ChannelTypeGuildCategory {
|
||||||
err = portal.CreateMatrixRoom(user, ch)
|
err = portal.CreateMatrixRoom(user, ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Warnfln("Error creating room for guild channel %s: %v", ch.ID, err)
|
log.Error().Err(err).Str("channel_id", ch.ID).
|
||||||
|
Msg("Failed to create room for guild channel while bridging guild")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guild.AutoBridgeChannels = everything
|
if everything {
|
||||||
|
guild.BridgingMode = database.GuildBridgeEverything
|
||||||
|
}
|
||||||
guild.Update()
|
guild.Update()
|
||||||
|
|
||||||
|
if user.Session.IsUser {
|
||||||
|
log.Debug().Msg("Subscribing to guild after bridging")
|
||||||
|
err = user.Session.SubscribeGuild(discordgo.GuildSubscribeData{
|
||||||
|
GuildID: guild.ID,
|
||||||
|
Typing: true,
|
||||||
|
Activities: true,
|
||||||
|
Threads: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to subscribe to guild")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,10 +1309,10 @@ func (user *User) unbridgeGuild(guildID string) error {
|
|||||||
}
|
}
|
||||||
guild.roomCreateLock.Lock()
|
guild.roomCreateLock.Lock()
|
||||||
defer guild.roomCreateLock.Unlock()
|
defer guild.roomCreateLock.Unlock()
|
||||||
if !guild.AutoBridgeChannels && guild.MXID == "" {
|
if guild.BridgingMode == database.GuildBridgeNothing && guild.MXID == "" {
|
||||||
return errors.New("that guild is not bridged")
|
return errors.New("that guild is not bridged")
|
||||||
}
|
}
|
||||||
guild.AutoBridgeChannels = false
|
guild.BridgingMode = database.GuildBridgeNothing
|
||||||
guild.Update()
|
guild.Update()
|
||||||
for _, portal := range user.bridge.GetAllPortalsInGuild(guild.ID) {
|
for _, portal := range user.bridge.GetAllPortalsInGuild(guild.ID) {
|
||||||
portal.cleanup(false)
|
portal.cleanup(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user