forked from mirror/mautrix-discord
Switch startup, config, commands and db migrations to mautrix-go systems
This commit is contained in:
parent
cf5384d908
commit
9f9f7ca4fd
74 changed files with 3470 additions and 5682 deletions
|
@ -1,4 +1,4 @@
|
||||||
package bridge
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -15,7 +15,7 @@ import (
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
|
func (portal *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
|
||||||
// We might want to make this save to disk in the future. Discord defaults
|
// We might want to make this save to disk in the future. Discord defaults
|
||||||
// to 8mb for all attachments to a messages for non-nitro users and
|
// to 8mb for all attachments to a messages for non-nitro users and
|
||||||
// non-boosted servers.
|
// non-boosted servers.
|
||||||
|
@ -42,7 +42,7 @@ func (p *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
|
||||||
return ioutil.ReadAll(resp.Body)
|
return ioutil.ReadAll(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Portal) downloadMatrixAttachment(eventID id.EventID, content *event.MessageEventContent) ([]byte, error) {
|
func (portal *Portal) downloadMatrixAttachment(eventID id.EventID, content *event.MessageEventContent) ([]byte, error) {
|
||||||
var file *event.EncryptedFileInfo
|
var file *event.EncryptedFileInfo
|
||||||
rawMXC := content.URL
|
rawMXC := content.URL
|
||||||
|
|
||||||
|
@ -53,22 +53,22 @@ func (p *Portal) downloadMatrixAttachment(eventID id.EventID, content *event.Mes
|
||||||
|
|
||||||
mxc, err := rawMXC.Parse()
|
mxc, err := rawMXC.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Errorln("Malformed content URL in %s: %v", eventID, err)
|
portal.log.Errorln("Malformed content URL in %s: %v", eventID, err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := p.MainIntent().DownloadBytes(mxc)
|
data, err := portal.MainIntent().DownloadBytes(mxc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Errorfln("Failed to download media in %s: %v", eventID, err)
|
portal.log.Errorfln("Failed to download media in %s: %v", eventID, err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if file != nil {
|
if file != nil {
|
||||||
data, err = file.Decrypt(data)
|
err = file.DecryptInPlace(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err)
|
portal.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,13 +76,13 @@ func (p *Portal) downloadMatrixAttachment(eventID id.EventID, content *event.Mes
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error {
|
func (portal *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error {
|
||||||
req := mautrix.ReqUploadMedia{
|
req := mautrix.ReqUploadMedia{
|
||||||
ContentBytes: data,
|
ContentBytes: data,
|
||||||
ContentType: content.Info.MimeType,
|
ContentType: content.Info.MimeType,
|
||||||
}
|
}
|
||||||
var mxc id.ContentURI
|
var mxc id.ContentURI
|
||||||
if p.bridge.Config.Homeserver.AsyncMedia {
|
if portal.bridge.Config.Homeserver.AsyncMedia {
|
||||||
uploaded, err := intent.UnstableUploadAsync(req)
|
uploaded, err := intent.UnstableUploadAsync(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
|
@ -1,4 +1,4 @@
|
||||||
package bridge
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -1,42 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b *Bridge) updateBotProfile() {
|
|
||||||
cfg := b.Config.Appservice.Bot
|
|
||||||
|
|
||||||
// Set the bot's avatar.
|
|
||||||
if cfg.Avatar != "" {
|
|
||||||
var err error
|
|
||||||
var mxc id.ContentURI
|
|
||||||
|
|
||||||
if cfg.Avatar == "remove" {
|
|
||||||
err = b.bot.SetAvatarURL(mxc)
|
|
||||||
} else {
|
|
||||||
mxc, err = id.ParseContentURI(cfg.Avatar)
|
|
||||||
if err == nil {
|
|
||||||
err = b.bot.SetAvatarURL(mxc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
b.log.Warnln("failed to update the bot's avatar: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the bot's display name.
|
|
||||||
if cfg.Displayname != "" {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if cfg.Displayname == "remove" {
|
|
||||||
err = b.bot.SetDisplayName("")
|
|
||||||
} else {
|
|
||||||
err = b.bot.SetDisplayName(cfg.Displayname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
b.log.Warnln("failed to update the bot's display name", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
203
bridge/bridge.go
203
bridge/bridge.go
|
@ -1,203 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/config"
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
|
||||||
"go.mau.fi/mautrix-discord/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
reconnectDelay = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type Bridge struct {
|
|
||||||
Config *config.Config
|
|
||||||
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
as *appservice.AppService
|
|
||||||
db *database.Database
|
|
||||||
eventProcessor *appservice.EventProcessor
|
|
||||||
matrixHandler *matrixHandler
|
|
||||||
bot *appservice.IntentAPI
|
|
||||||
provisioning *ProvisioningAPI
|
|
||||||
|
|
||||||
usersByMXID map[id.UserID]*User
|
|
||||||
usersByID map[string]*User
|
|
||||||
usersLock sync.Mutex
|
|
||||||
|
|
||||||
managementRooms map[id.RoomID]*User
|
|
||||||
managementRoomsLock sync.Mutex
|
|
||||||
|
|
||||||
portalsByMXID map[id.RoomID]*Portal
|
|
||||||
portalsByID map[database.PortalKey]*Portal
|
|
||||||
portalsLock sync.Mutex
|
|
||||||
|
|
||||||
puppets map[string]*Puppet
|
|
||||||
puppetsByCustomMXID map[id.UserID]*Puppet
|
|
||||||
puppetsLock sync.Mutex
|
|
||||||
|
|
||||||
StateStore *database.SQLStateStore
|
|
||||||
|
|
||||||
crypto Crypto
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(cfg *config.Config) (*Bridge, error) {
|
|
||||||
// Create the logger.
|
|
||||||
logger, err := cfg.CreateLogger()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infoln("Initializing version", version.String)
|
|
||||||
|
|
||||||
// Create and initialize the app service.
|
|
||||||
appservice, err := cfg.CreateAppService()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
appservice.Log = log.Sub("matrix")
|
|
||||||
|
|
||||||
appservice.Init()
|
|
||||||
|
|
||||||
// Create the bot.
|
|
||||||
bot := appservice.BotIntent()
|
|
||||||
|
|
||||||
// Setup the database.
|
|
||||||
db, err := cfg.CreateDatabase(logger)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the state store
|
|
||||||
logger.Debugln("Initializing state store")
|
|
||||||
stateStore := database.NewSQLStateStore(db)
|
|
||||||
appservice.StateStore = stateStore
|
|
||||||
|
|
||||||
// Create the bridge.
|
|
||||||
bridge := &Bridge{
|
|
||||||
as: appservice,
|
|
||||||
db: db,
|
|
||||||
bot: bot,
|
|
||||||
Config: cfg,
|
|
||||||
log: logger,
|
|
||||||
|
|
||||||
usersByMXID: make(map[id.UserID]*User),
|
|
||||||
usersByID: make(map[string]*User),
|
|
||||||
|
|
||||||
managementRooms: make(map[id.RoomID]*User),
|
|
||||||
|
|
||||||
portalsByMXID: make(map[id.RoomID]*Portal),
|
|
||||||
portalsByID: make(map[database.PortalKey]*Portal),
|
|
||||||
|
|
||||||
puppets: make(map[string]*Puppet),
|
|
||||||
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
|
||||||
|
|
||||||
StateStore: stateStore,
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.crypto = NewCryptoHelper(bridge)
|
|
||||||
|
|
||||||
if cfg.Appservice.Provisioning.Enabled() {
|
|
||||||
bridge.provisioning = newProvisioningAPI(bridge)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the event processors
|
|
||||||
bridge.setupEvents()
|
|
||||||
|
|
||||||
return bridge, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) connect() error {
|
|
||||||
b.log.Debugln("Checking connection to homeserver")
|
|
||||||
|
|
||||||
for {
|
|
||||||
resp, err := b.bot.Whoami()
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, mautrix.MUnknownToken) {
|
|
||||||
b.log.Fatalln("Access token invalid. Is the registration installed in your homeserver correctly?")
|
|
||||||
|
|
||||||
return fmt.Errorf("invalid access token")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.log.Errorfln("Failed to connect to homeserver : %v", err)
|
|
||||||
b.log.Errorfln("reconnecting in %s", reconnectDelay)
|
|
||||||
|
|
||||||
time.Sleep(reconnectDelay)
|
|
||||||
} else if resp.UserID != b.bot.UserID {
|
|
||||||
b.log.Fatalln("Unexpected user ID in whoami call: got %s, expected %s", resp.UserID, b.bot.UserID)
|
|
||||||
|
|
||||||
return fmt.Errorf("expected user id %q but got %q", b.bot.UserID, resp.UserID)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.log.Debugln("Connected to homeserver")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Start() error {
|
|
||||||
b.log.Infoln("Bridge started")
|
|
||||||
|
|
||||||
if err := b.connect(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.crypto != nil {
|
|
||||||
if err := b.crypto.Init(); err != nil {
|
|
||||||
b.log.Fatalln("Error initializing end-to-bridge encryption:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.log.Debugln("Starting application service HTTP server")
|
|
||||||
go b.as.Start()
|
|
||||||
|
|
||||||
b.log.Debugln("Starting event processor")
|
|
||||||
go b.eventProcessor.Start()
|
|
||||||
|
|
||||||
go b.updateBotProfile()
|
|
||||||
|
|
||||||
if b.crypto != nil {
|
|
||||||
go b.crypto.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
go b.startUsers()
|
|
||||||
|
|
||||||
// Finally tell the appservice we're ready
|
|
||||||
b.as.Ready = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Stop() {
|
|
||||||
if b.crypto != nil {
|
|
||||||
b.crypto.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.as.Stop()
|
|
||||||
b.eventProcessor.Stop()
|
|
||||||
|
|
||||||
for _, user := range b.usersByMXID {
|
|
||||||
if user.Session == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b.log.Debugln("Disconnecting", user.MXID)
|
|
||||||
user.Session.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.log.Infoln("Bridge stopped")
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
|
||||||
"github.com/google/shlex"
|
|
||||||
|
|
||||||
"maunium.net/go/maulogger/v2"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type commandHandler struct {
|
|
||||||
bridge *Bridge
|
|
||||||
log maulogger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCommandHandler(bridge *Bridge) *commandHandler {
|
|
||||||
return &commandHandler{
|
|
||||||
bridge: bridge,
|
|
||||||
log: bridge.log.Sub("Commands"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandsHelpPrinter(options kong.HelpOptions, ctx *kong.Context) error {
|
|
||||||
selected := ctx.Selected()
|
|
||||||
|
|
||||||
if selected == nil {
|
|
||||||
for _, cmd := range ctx.Model.Leaves(true) {
|
|
||||||
fmt.Fprintf(ctx.Stdout, " * %s - %s\n", cmd.Path(), cmd.Help)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(ctx.Stdout, "%s - %s\n", selected.Path(), selected.Help)
|
|
||||||
if selected.Detail != "" {
|
|
||||||
fmt.Fprintf(ctx.Stdout, "\n%s\n", selected.Detail)
|
|
||||||
}
|
|
||||||
if len(selected.Positional) > 0 {
|
|
||||||
fmt.Fprintf(ctx.Stdout, "\nArguments:\n")
|
|
||||||
for _, arg := range selected.Positional {
|
|
||||||
fmt.Fprintf(ctx.Stdout, "%s %s\n", arg.Summary(), arg.Help)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *commandHandler) handle(roomID id.RoomID, user *User, message string, replyTo id.EventID) {
|
|
||||||
cmd := commands{
|
|
||||||
globals: globals{
|
|
||||||
bot: h.bridge.bot,
|
|
||||||
bridge: h.bridge,
|
|
||||||
portal: h.bridge.GetPortalByMXID(roomID),
|
|
||||||
handler: h,
|
|
||||||
roomID: roomID,
|
|
||||||
user: user,
|
|
||||||
replyTo: replyTo,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &strings.Builder{}
|
|
||||||
|
|
||||||
parse, err := kong.New(
|
|
||||||
&cmd,
|
|
||||||
kong.Exit(func(int) {}),
|
|
||||||
kong.NoDefaultHelp(),
|
|
||||||
kong.Writers(buf, buf),
|
|
||||||
kong.Help(commandsHelpPrinter),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
h.log.Warnf("Failed to create argument parser for %q: %v", roomID, err)
|
|
||||||
|
|
||||||
cmd.globals.reply("unexpected error, please try again shortly")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := shlex.Split(message)
|
|
||||||
if err != nil {
|
|
||||||
h.log.Warnf("Failed to split message %q: %v", message, err)
|
|
||||||
|
|
||||||
cmd.globals.reply("failed to process the command")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, err := parse.Parse(args)
|
|
||||||
if err != nil {
|
|
||||||
h.log.Warnf("Failed to parse command %q: %v", message, err)
|
|
||||||
|
|
||||||
cmd.globals.reply(fmt.Sprintf("failed to process the command: %v", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.globals.context = ctx
|
|
||||||
|
|
||||||
err = ctx.Run(&cmd.globals)
|
|
||||||
if err != nil {
|
|
||||||
h.log.Warnf("Command %q failed: %v", message, err)
|
|
||||||
|
|
||||||
output := buf.String()
|
|
||||||
if output != "" {
|
|
||||||
cmd.globals.reply(output)
|
|
||||||
} else {
|
|
||||||
cmd.globals.reply("unexpected failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf.Len() > 0 {
|
|
||||||
cmd.globals.reply(buf.String())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,360 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/format"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/consts"
|
|
||||||
"go.mau.fi/mautrix-discord/remoteauth"
|
|
||||||
"go.mau.fi/mautrix-discord/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
type globals struct {
|
|
||||||
context *kong.Context
|
|
||||||
|
|
||||||
bridge *Bridge
|
|
||||||
bot *appservice.IntentAPI
|
|
||||||
portal *Portal
|
|
||||||
handler *commandHandler
|
|
||||||
roomID id.RoomID
|
|
||||||
user *User
|
|
||||||
replyTo id.EventID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *globals) reply(msg string) {
|
|
||||||
content := format.RenderMarkdown(msg, true, false)
|
|
||||||
content.MsgType = event.MsgNotice
|
|
||||||
intent := g.bot
|
|
||||||
|
|
||||||
if g.portal != nil && g.portal.IsPrivateChat() {
|
|
||||||
intent = g.portal.MainIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := intent.SendMessageEvent(g.roomID, event.EventMessage, content)
|
|
||||||
if err != nil {
|
|
||||||
g.handler.log.Warnfln("Failed to reply to command from %q: %v", g.user.MXID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type commands struct {
|
|
||||||
globals
|
|
||||||
|
|
||||||
Disconnect disconnectCmd `kong:"cmd,help='Disconnect from Discord'"`
|
|
||||||
Help helpCmd `kong:"cmd,help='Displays this message.'"`
|
|
||||||
Login loginCmd `kong:"cmd,help='Log in to Discord.'"`
|
|
||||||
Logout logoutCmd `kong:"cmd,help='Log out of Discord.'"`
|
|
||||||
Reconnect reconnectCmd `kong:"cmd,help='Reconnect to Discord'"`
|
|
||||||
Version versionCmd `kong:"cmd,help='Displays the version of the bridge.'"`
|
|
||||||
|
|
||||||
Guilds guildsCmd `kong:"cmd,help='Guild bridging management.'"`
|
|
||||||
|
|
||||||
LoginMatrix loginMatrixCmd `kong:"cmd,help='Replace the puppet for your Discord account with your real Matrix account.'"`
|
|
||||||
LogoutMatrix logoutMatrixCmd `kong:"cmd,help='Switch the puppet for your Discord account back to the default one.'"`
|
|
||||||
PingMatrix pingMatrixCmd `kong:"cmd,help='check if your double puppet is working properly'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Help Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type helpCmd struct {
|
|
||||||
Command []string `kong:"arg,optional,help='The command to get help on.'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *helpCmd) Run(g *globals) error {
|
|
||||||
ctx, err := kong.Trace(g.context.Kong, c.Command)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Error != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ctx.PrintUsage(true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(g.context.Stdout)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Version Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type versionCmd struct{}
|
|
||||||
|
|
||||||
func (c *versionCmd) Run(g *globals) error {
|
|
||||||
fmt.Fprintln(g.context.Stdout, consts.Name, version.String)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Login Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type loginCmd struct{}
|
|
||||||
|
|
||||||
func (l *loginCmd) Run(g *globals) error {
|
|
||||||
if g.user.LoggedIn() {
|
|
||||||
fmt.Fprintf(g.context.Stdout, "You are already logged in")
|
|
||||||
|
|
||||||
return fmt.Errorf("user already logged in")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := remoteauth.New()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
qrChan := make(chan string)
|
|
||||||
doneChan := make(chan struct{})
|
|
||||||
|
|
||||||
var qrCodeEvent id.EventID
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
code := <-qrChan
|
|
||||||
|
|
||||||
resp, err := g.user.sendQRCode(g.bot, g.roomID, code)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Failed to generate the qrcode")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
qrCodeEvent = resp
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if err := client.Dial(ctx, qrChan, doneChan); err != nil {
|
|
||||||
close(qrChan)
|
|
||||||
close(doneChan)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
<-doneChan
|
|
||||||
|
|
||||||
if qrCodeEvent != "" {
|
|
||||||
_, err := g.bot.RedactEvent(g.roomID, qrCodeEvent)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Errorf("Failed to redact the qrcode: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := client.Result()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Failed to log in")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.user.Login(user.Token); err != nil {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Failed to login", err)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
g.user.Lock()
|
|
||||||
g.user.ID = user.UserID
|
|
||||||
g.user.Update()
|
|
||||||
g.user.Unlock()
|
|
||||||
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Successfully logged in")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Logout Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type logoutCmd struct{}
|
|
||||||
|
|
||||||
func (l *logoutCmd) Run(g *globals) error {
|
|
||||||
if !g.user.LoggedIn() {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "You are not logged in")
|
|
||||||
|
|
||||||
return fmt.Errorf("user is not logged in")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := g.user.Logout()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Failed to log out")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Successfully logged out")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Disconnect Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type disconnectCmd struct{}
|
|
||||||
|
|
||||||
func (d *disconnectCmd) Run(g *globals) error {
|
|
||||||
if !g.user.Connected() {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "You are not connected")
|
|
||||||
|
|
||||||
return fmt.Errorf("user is not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.user.Disconnect(); err != nil {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Failed to disconnect")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Successfully disconnected")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Reconnect Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type reconnectCmd struct{}
|
|
||||||
|
|
||||||
func (r *reconnectCmd) Run(g *globals) error {
|
|
||||||
if g.user.Connected() {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "You are already connected")
|
|
||||||
|
|
||||||
return fmt.Errorf("user is already connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.user.Connect(); err != nil {
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Failed to connect")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(g.context.Stdout, "Successfully connected")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// LoginMatrix Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type loginMatrixCmd struct {
|
|
||||||
AccessToken string `kong:"arg,help='The shared secret to use the bridge'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *loginMatrixCmd) Run(g *globals) error {
|
|
||||||
puppet := g.bridge.GetPuppetByID(g.user.ID)
|
|
||||||
|
|
||||||
err := puppet.SwitchCustomMXID(m.AccessToken, g.user.MXID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(g.context.Stdout, "Failed to switch puppet: %v", err)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(g.context.Stdout, "Successfully switched puppet")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// LogoutMatrix Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type logoutMatrixCmd struct{}
|
|
||||||
|
|
||||||
func (m *logoutMatrixCmd) Run(g *globals) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// PingMatrix Command
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type pingMatrixCmd struct{}
|
|
||||||
|
|
||||||
func (m *pingMatrixCmd) Run(g *globals) error {
|
|
||||||
puppet := g.bridge.GetPuppetByCustomMXID(g.user.MXID)
|
|
||||||
if puppet == nil || puppet.CustomIntent() == nil {
|
|
||||||
fmt.Fprintf(g.context.Stdout, "You have not changed your Discord account's Matrix puppet.")
|
|
||||||
|
|
||||||
return fmt.Errorf("double puppet not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := puppet.CustomIntent().Whoami()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(g.context.Stdout, "Failed to validate Matrix login: %v", err)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(g.context.Stdout, "Confirmed valid access token for %s / %s", resp.UserID, resp.DeviceID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Guilds Commands
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
type guildsCmd struct {
|
|
||||||
Status guildStatusCmd `kong:"cmd,help='Show the bridge status for the guilds you are in'"`
|
|
||||||
Bridge guildBridgeCmd `kong:"cmd,help='Bridge a guild'"`
|
|
||||||
Unbridge guildUnbridgeCmd `kong:"cmd,help='Unbridge a guild'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type guildStatusCmd struct{}
|
|
||||||
|
|
||||||
func (c *guildStatusCmd) Run(g *globals) error {
|
|
||||||
g.user.guildsLock.Lock()
|
|
||||||
defer g.user.guildsLock.Unlock()
|
|
||||||
|
|
||||||
if len(g.user.guilds) == 0 {
|
|
||||||
fmt.Fprintf(g.context.Stdout, "you haven't joined any guilds.")
|
|
||||||
} else {
|
|
||||||
for _, guild := range g.user.guilds {
|
|
||||||
status := "not bridged"
|
|
||||||
if guild.Bridge {
|
|
||||||
status = "bridged"
|
|
||||||
}
|
|
||||||
fmt.Fprintf(g.context.Stdout, "%s %s %s\n", guild.GuildName, guild.GuildID, status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type guildBridgeCmd struct {
|
|
||||||
GuildID string `kong:"arg,help='the id of the guild to unbridge'"`
|
|
||||||
Entire bool `kong:"flag,help='whether or not to bridge all channels'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *guildBridgeCmd) Run(g *globals) error {
|
|
||||||
if err := g.user.bridgeGuild(c.GuildID, c.Entire); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(g.context.Stdout, "Successfully bridged guild %s", c.GuildID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type guildUnbridgeCmd struct {
|
|
||||||
GuildID string `kong:"arg,help='the id of the guild to unbridge'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *guildUnbridgeCmd) Run(g *globals) error {
|
|
||||||
if err := g.user.unbridgeGuild(c.GuildID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(g.context.Stdout, "Successfully unbridged guild %s", c.GuildID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
339
bridge/crypto.go
339
bridge/crypto.go
|
@ -1,339 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime/debug"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"maunium.net/go/maulogger/v2"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/crypto"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
var NoSessionFound = crypto.NoSessionFound
|
|
||||||
|
|
||||||
var levelTrace = maulogger.Level{
|
|
||||||
Name: "TRACE",
|
|
||||||
Severity: -10,
|
|
||||||
Color: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Crypto interface {
|
|
||||||
HandleMemberEvent(*event.Event)
|
|
||||||
Decrypt(*event.Event) (*event.Event, error)
|
|
||||||
Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error)
|
|
||||||
WaitForSession(id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool
|
|
||||||
RequestSession(id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID)
|
|
||||||
ResetSession(id.RoomID)
|
|
||||||
Init() error
|
|
||||||
Start()
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
type CryptoHelper struct {
|
|
||||||
bridge *Bridge
|
|
||||||
client *mautrix.Client
|
|
||||||
mach *crypto.OlmMachine
|
|
||||||
store *database.SQLCryptoStore
|
|
||||||
log maulogger.Logger
|
|
||||||
baseLog maulogger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCryptoHelper(bridge *Bridge) Crypto {
|
|
||||||
if !bridge.Config.Bridge.Encryption.Allow {
|
|
||||||
bridge.log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
baseLog := bridge.log.Sub("Crypto")
|
|
||||||
return &CryptoHelper{
|
|
||||||
bridge: bridge,
|
|
||||||
log: baseLog.Sub("Helper"),
|
|
||||||
baseLog: baseLog,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) Init() error {
|
|
||||||
helper.log.Debugln("Initializing end-to-bridge encryption...")
|
|
||||||
|
|
||||||
helper.store = database.NewSQLCryptoStore(helper.bridge.db, helper.bridge.as.BotMXID(),
|
|
||||||
fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.as.HomeserverDomain))
|
|
||||||
|
|
||||||
var err error
|
|
||||||
helper.client, err = helper.loginBot()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
helper.log.Debugln("Logged in as bridge bot with device ID", helper.client.DeviceID)
|
|
||||||
|
|
||||||
logger := &cryptoLogger{helper.baseLog}
|
|
||||||
stateStore := &cryptoStateStore{helper.bridge}
|
|
||||||
helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore)
|
|
||||||
helper.mach.AllowKeyShare = helper.allowKeyShare
|
|
||||||
|
|
||||||
helper.client.Syncer = &cryptoSyncer{helper.mach}
|
|
||||||
helper.client.Store = &cryptoClientStore{helper.store}
|
|
||||||
|
|
||||||
return helper.mach.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info event.RequestedKeyInfo) *crypto.KeyShareRejection {
|
|
||||||
cfg := helper.bridge.Config.Bridge.Encryption.KeySharing
|
|
||||||
if !cfg.Allow {
|
|
||||||
return &crypto.KeyShareRejectNoResponse
|
|
||||||
} else if device.Trust == crypto.TrustStateBlacklisted {
|
|
||||||
return &crypto.KeyShareRejectBlacklisted
|
|
||||||
} else if device.Trust == crypto.TrustStateVerified || !cfg.RequireVerification {
|
|
||||||
portal := helper.bridge.GetPortalByMXID(info.RoomID)
|
|
||||||
if portal == nil {
|
|
||||||
helper.log.Debugfln("Rejecting key request for %s from %s/%s: room is not a portal", info.SessionID, device.UserID, device.DeviceID)
|
|
||||||
|
|
||||||
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"}
|
|
||||||
}
|
|
||||||
user := helper.bridge.GetUserByMXID(device.UserID)
|
|
||||||
// FIXME reimplement IsInPortal
|
|
||||||
if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ {
|
|
||||||
helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID)
|
|
||||||
|
|
||||||
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"}
|
|
||||||
}
|
|
||||||
helper.log.Debugfln("Accepting key request for %s from %s/%s", info.SessionID, device.UserID, device.DeviceID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &crypto.KeyShareRejectUnverified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) {
|
|
||||||
deviceID := helper.store.FindDeviceID()
|
|
||||||
if len(deviceID) > 0 {
|
|
||||||
helper.log.Debugln("Found existing device ID for bot in database:", deviceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := mautrix.NewClient(helper.bridge.as.HomeserverURL, "", "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to initialize client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Logger = helper.baseLog.Sub("Bot")
|
|
||||||
client.Client = helper.bridge.as.HTTPClient
|
|
||||||
client.DefaultHTTPRetries = helper.bridge.as.DefaultHTTPRetries
|
|
||||||
flows, err := client.GetLoginFlows()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get supported login flows: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
flow := flows.FirstFlowOfType(mautrix.AuthTypeAppservice, mautrix.AuthTypeHalfyAppservice)
|
|
||||||
if flow == nil {
|
|
||||||
return nil, fmt.Errorf("homeserver does not support appservice login")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We set the API token to the AS token here to authenticate the appservice login
|
|
||||||
// It'll get overridden after the login
|
|
||||||
client.AccessToken = helper.bridge.as.Registration.AppToken
|
|
||||||
resp, err := client.Login(&mautrix.ReqLogin{
|
|
||||||
Type: flow.Type,
|
|
||||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.as.BotMXID())},
|
|
||||||
DeviceID: deviceID,
|
|
||||||
InitialDeviceDisplayName: "Discord Bridge",
|
|
||||||
StoreCredentials: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to log in as bridge bot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
helper.store.DeviceID = resp.DeviceID
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) Start() {
|
|
||||||
helper.log.Debugln("Starting syncer for receiving to-device messages")
|
|
||||||
|
|
||||||
err := helper.client.Sync()
|
|
||||||
if err != nil {
|
|
||||||
helper.log.Errorln("Fatal error syncing:", err)
|
|
||||||
} else {
|
|
||||||
helper.log.Infoln("Bridge bot to-device syncer stopped without error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) Stop() {
|
|
||||||
helper.log.Debugln("CryptoHelper.Stop() called, stopping bridge bot sync")
|
|
||||||
helper.client.StopSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) {
|
|
||||||
return helper.mach.DecryptMegolmEvent(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) {
|
|
||||||
encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, &content)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
helper.log.Debugfln("Got %v while encrypting event for %s, sharing group session and trying again...", err, roomID)
|
|
||||||
users, err := helper.store.GetRoomMembers(roomID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get room member list: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = helper.mach.ShareGroupSession(roomID, users)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to share group session: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, &content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to encrypt event after re-sharing group session: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return encrypted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool {
|
|
||||||
return helper.mach.WaitForSession(roomID, senderKey, sessionID, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, userID id.UserID, deviceID id.DeviceID) {
|
|
||||||
err := helper.mach.SendRoomKeyRequest(roomID, senderKey, sessionID, "", map[id.UserID][]id.DeviceID{userID: {deviceID}})
|
|
||||||
if err != nil {
|
|
||||||
helper.log.Warnfln("Failed to send key request to %s/%s for %s in %s: %v", userID, deviceID, sessionID, roomID, err)
|
|
||||||
} else {
|
|
||||||
helper.log.Debugfln("Sent key request to %s/%s for %s in %s", userID, deviceID, sessionID, roomID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) ResetSession(roomID id.RoomID) {
|
|
||||||
err := helper.mach.CryptoStore.RemoveOutboundGroupSession(roomID)
|
|
||||||
if err != nil {
|
|
||||||
helper.log.Debugfln("Error manually removing outbound group session in %s: %v", roomID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (helper *CryptoHelper) HandleMemberEvent(evt *event.Event) {
|
|
||||||
helper.mach.HandleMemberEvent(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
type cryptoSyncer struct {
|
|
||||||
*crypto.OlmMachine
|
|
||||||
}
|
|
||||||
|
|
||||||
func (syncer *cryptoSyncer) ProcessResponse(resp *mautrix.RespSync, since string) error {
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
syncer.Log.Error("Processing sync response (%s) panicked: %v\n%s", since, err, debug.Stack())
|
|
||||||
}
|
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
syncer.Log.Trace("Starting sync response handling (%s)", since)
|
|
||||||
syncer.ProcessSyncResponse(resp, since)
|
|
||||||
syncer.Log.Trace("Successfully handled sync response (%s)", since)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(30 * time.Second):
|
|
||||||
syncer.Log.Warn("Handling sync response (%s) is taking unusually long", since)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (syncer *cryptoSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
|
||||||
syncer.Log.Error("Error /syncing, waiting 10 seconds: %v", err)
|
|
||||||
|
|
||||||
return 10 * time.Second, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (syncer *cryptoSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
|
||||||
everything := []event.Type{{Type: "*"}}
|
|
||||||
|
|
||||||
return &mautrix.Filter{
|
|
||||||
Presence: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
AccountData: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
Room: mautrix.RoomFilter{
|
|
||||||
IncludeLeave: false,
|
|
||||||
Ephemeral: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
AccountData: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
State: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
Timeline: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type cryptoLogger struct {
|
|
||||||
int maulogger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Error(message string, args ...interface{}) {
|
|
||||||
c.int.Errorfln(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Warn(message string, args ...interface{}) {
|
|
||||||
c.int.Warnfln(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Debug(message string, args ...interface{}) {
|
|
||||||
c.int.Debugfln(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Trace(message string, args ...interface{}) {
|
|
||||||
c.int.Logfln(levelTrace, message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
type cryptoClientStore struct {
|
|
||||||
int *database.SQLCryptoStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cryptoClientStore) SaveFilterID(_ id.UserID, _ string) {}
|
|
||||||
func (c cryptoClientStore) LoadFilterID(_ id.UserID) string { return "" }
|
|
||||||
func (c cryptoClientStore) SaveRoom(_ *mautrix.Room) {}
|
|
||||||
func (c cryptoClientStore) LoadRoom(_ id.RoomID) *mautrix.Room { return nil }
|
|
||||||
|
|
||||||
func (c cryptoClientStore) SaveNextBatch(_ id.UserID, nextBatchToken string) {
|
|
||||||
c.int.PutNextBatch(nextBatchToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cryptoClientStore) LoadNextBatch(_ id.UserID) string {
|
|
||||||
return c.int.GetNextBatch()
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ mautrix.Storer = (*cryptoClientStore)(nil)
|
|
||||||
|
|
||||||
type cryptoStateStore struct {
|
|
||||||
bridge *Bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ crypto.StateStore = (*cryptoStateStore)(nil)
|
|
||||||
|
|
||||||
func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool {
|
|
||||||
portal := c.bridge.GetPortalByMXID(id)
|
|
||||||
if portal != nil {
|
|
||||||
return portal.Encrypted
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoStateStore) FindSharedRooms(id id.UserID) []id.RoomID {
|
|
||||||
return c.bridge.StateStore.FindSharedRooms(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoStateStore) GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent {
|
|
||||||
// TODO implement
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,337 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoCustomMXID = errors.New("no custom mxid set")
|
|
||||||
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
|
|
||||||
)
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// additional bridge api
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
func (b *Bridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
|
|
||||||
_, homeserver, err := mxid.Parse()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
homeserverURL, found := b.Config.Bridge.DoublePuppetServerMap[homeserver]
|
|
||||||
if !found {
|
|
||||||
if homeserver == b.as.HomeserverDomain {
|
|
||||||
homeserverURL = b.as.HomeserverURL
|
|
||||||
} else if b.Config.Bridge.DoublePuppetAllowDiscovery {
|
|
||||||
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
homeserverURL = resp.Homeserver.BaseURL
|
|
||||||
b.log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid)
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := mautrix.NewClient(homeserverURL, mxid, accessToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Logger = b.as.Log.Sub(mxid.String())
|
|
||||||
client.Client = b.as.HTTPClient
|
|
||||||
client.DefaultHTTPRetries = b.as.DefaultHTTPRetries
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// mautrix.Syncer implementation
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
func (p *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
|
||||||
everything := []event.Type{{Type: "*"}}
|
|
||||||
return &mautrix.Filter{
|
|
||||||
Presence: mautrix.FilterPart{
|
|
||||||
Senders: []id.UserID{p.CustomMXID},
|
|
||||||
Types: []event.Type{event.EphemeralEventPresence},
|
|
||||||
},
|
|
||||||
AccountData: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
Room: mautrix.RoomFilter{
|
|
||||||
Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
|
|
||||||
IncludeLeave: false,
|
|
||||||
AccountData: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
State: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
Timeline: mautrix.FilterPart{NotTypes: everything},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
|
||||||
p.log.Warnln("Sync error:", err)
|
|
||||||
if errors.Is(err, mautrix.MUnknownToken) {
|
|
||||||
if !p.tryRelogin(err, "syncing") {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.customIntent.AccessToken = p.AccessToken
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 10 * time.Second, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
|
|
||||||
if !p.customUser.LoggedIn() {
|
|
||||||
p.log.Debugln("Skipping sync processing: custom user not connected to discord")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// for roomID, events := range resp.Rooms.Join {
|
|
||||||
// for _, evt := range events.Ephemeral.Events {
|
|
||||||
// evt.RoomID = roomID
|
|
||||||
// err := evt.Content.ParseRaw(evt.Type)
|
|
||||||
// if err != nil {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// switch evt.Type {
|
|
||||||
// case event.EphemeralEventReceipt:
|
|
||||||
// if p.EnableReceipts {
|
|
||||||
// go p.bridge.matrixHandler.HandleReceipt(evt)
|
|
||||||
// }
|
|
||||||
// case event.EphemeralEventTyping:
|
|
||||||
// go p.bridge.matrixHandler.HandleTyping(evt)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if p.EnablePresence {
|
|
||||||
// for _, evt := range resp.Presence.Events {
|
|
||||||
// if evt.Sender != p.CustomMXID {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// err := evt.Content.ParseRaw(evt.Type)
|
|
||||||
// if err != nil {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// go p.bridge.matrixHandler.HandlePresence(evt)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// mautrix.Storer implementation
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
func (p *Puppet) SaveFilterID(_ id.UserID, _ string) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) SaveNextBatch(_ id.UserID, nbt string) {
|
|
||||||
p.NextBatch = nbt
|
|
||||||
p.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) SaveRoom(_ *mautrix.Room) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) LoadFilterID(_ id.UserID) string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) LoadNextBatch(_ id.UserID) string {
|
|
||||||
return p.NextBatch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// additional puppet api
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
func (p *Puppet) clearCustomMXID() {
|
|
||||||
p.CustomMXID = ""
|
|
||||||
p.AccessToken = ""
|
|
||||||
p.customIntent = nil
|
|
||||||
p.customUser = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
|
|
||||||
if p.CustomMXID == "" {
|
|
||||||
return nil, ErrNoCustomMXID
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := p.bridge.newDoublePuppetClient(p.CustomMXID, p.AccessToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Syncer = p
|
|
||||||
client.Store = p
|
|
||||||
|
|
||||||
ia := p.bridge.as.NewIntentAPI("custom")
|
|
||||||
ia.Client = client
|
|
||||||
ia.Localpart, _, _ = p.CustomMXID.Parse()
|
|
||||||
ia.UserID = p.CustomMXID
|
|
||||||
ia.IsCustomPuppet = true
|
|
||||||
|
|
||||||
return ia, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) StartCustomMXID(reloginOnFail bool) error {
|
|
||||||
if p.CustomMXID == "" {
|
|
||||||
p.clearCustomMXID()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
intent, err := p.newCustomIntent()
|
|
||||||
if err != nil {
|
|
||||||
p.clearCustomMXID()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := intent.Whoami()
|
|
||||||
if err != nil {
|
|
||||||
if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !p.tryRelogin(err, "initializing double puppeting")) {
|
|
||||||
p.clearCustomMXID()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
intent.AccessToken = p.AccessToken
|
|
||||||
} else if resp.UserID != p.CustomMXID {
|
|
||||||
p.clearCustomMXID()
|
|
||||||
|
|
||||||
return ErrMismatchingMXID
|
|
||||||
}
|
|
||||||
|
|
||||||
p.customIntent = intent
|
|
||||||
p.customUser = p.bridge.GetUserByMXID(p.CustomMXID)
|
|
||||||
p.startSyncing()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) tryRelogin(cause error, action string) bool {
|
|
||||||
if !p.bridge.Config.CanAutoDoublePuppet(p.CustomMXID) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
p.log.Debugfln("Trying to relogin after '%v' while %s", cause, action)
|
|
||||||
|
|
||||||
accessToken, err := p.loginWithSharedSecret(p.CustomMXID)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
p.log.Infofln("Successfully relogined after '%v' while %s", cause, action)
|
|
||||||
p.AccessToken = accessToken
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) startSyncing() {
|
|
||||||
if !p.bridge.Config.Bridge.SyncWithCustomPuppets {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
p.log.Debugln("Starting syncing...")
|
|
||||||
p.customIntent.SyncPresence = "offline"
|
|
||||||
|
|
||||||
err := p.customIntent.Sync()
|
|
||||||
if err != nil {
|
|
||||||
p.log.Errorln("Fatal error syncing:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) stopSyncing() {
|
|
||||||
if !p.bridge.Config.Bridge.SyncWithCustomPuppets {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.customIntent.StopSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
|
|
||||||
_, homeserver, _ := mxid.Parse()
|
|
||||||
|
|
||||||
p.log.Debugfln("Logging into %s with shared secret", mxid)
|
|
||||||
|
|
||||||
mac := hmac.New(sha512.New, []byte(p.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]))
|
|
||||||
mac.Write([]byte(mxid))
|
|
||||||
|
|
||||||
client, err := p.bridge.newDoublePuppetClient(mxid, "")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Login(&mautrix.ReqLogin{
|
|
||||||
Type: mautrix.AuthTypePassword,
|
|
||||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
|
|
||||||
Password: hex.EncodeToString(mac.Sum(nil)),
|
|
||||||
DeviceID: "Discord Bridge",
|
|
||||||
InitialDeviceDisplayName: "Discord Bridge",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.AccessToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
|
|
||||||
prevCustomMXID := p.CustomMXID
|
|
||||||
if p.customIntent != nil {
|
|
||||||
p.stopSyncing()
|
|
||||||
}
|
|
||||||
|
|
||||||
p.CustomMXID = mxid
|
|
||||||
p.AccessToken = accessToken
|
|
||||||
|
|
||||||
err := p.StartCustomMXID(false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if prevCustomMXID != "" {
|
|
||||||
delete(p.bridge.puppetsByCustomMXID, prevCustomMXID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.CustomMXID != "" {
|
|
||||||
p.bridge.puppetsByCustomMXID[p.CustomMXID] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
p.EnablePresence = p.bridge.Config.Bridge.DefaultBridgePresence
|
|
||||||
p.EnableReceipts = p.bridge.Config.Bridge.DefaultBridgeReceipts
|
|
||||||
|
|
||||||
p.bridge.as.StateStore.MarkRegistered(p.CustomMXID)
|
|
||||||
|
|
||||||
p.Update()
|
|
||||||
|
|
||||||
// TODO leave rooms with default puppet
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
376
bridge/matrix.go
376
bridge/matrix.go
|
@ -1,376 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"maunium.net/go/maulogger/v2"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/format"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type matrixHandler struct {
|
|
||||||
as *appservice.AppService
|
|
||||||
bridge *Bridge
|
|
||||||
log maulogger.Logger
|
|
||||||
cmd *commandHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) setupEvents() {
|
|
||||||
b.eventProcessor = appservice.NewEventProcessor(b.as)
|
|
||||||
|
|
||||||
b.matrixHandler = &matrixHandler{
|
|
||||||
as: b.as,
|
|
||||||
bridge: b,
|
|
||||||
log: b.log.Sub("Matrix"),
|
|
||||||
cmd: newCommandHandler(b),
|
|
||||||
}
|
|
||||||
|
|
||||||
b.eventProcessor.On(event.EventMessage, b.matrixHandler.handleMessage)
|
|
||||||
b.eventProcessor.On(event.EventEncrypted, b.matrixHandler.handleEncrypted)
|
|
||||||
b.eventProcessor.On(event.EventReaction, b.matrixHandler.handleReaction)
|
|
||||||
b.eventProcessor.On(event.EventRedaction, b.matrixHandler.handleRedaction)
|
|
||||||
b.eventProcessor.On(event.StateMember, b.matrixHandler.handleMembership)
|
|
||||||
b.eventProcessor.On(event.StateEncryption, b.matrixHandler.handleEncryption)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) join(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
|
|
||||||
resp, err := intent.JoinRoomByID(evt.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
members, err := intent.JoinedMembers(resp.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
intent.LeaveRoom(resp.RoomID)
|
|
||||||
|
|
||||||
mh.log.Debugfln("Failed to get members in room %s after accepting invite from %s as %s: %v", resp.RoomID, evt.Sender, intent.UserID, err)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(members.Joined) < 2 {
|
|
||||||
intent.LeaveRoom(resp.RoomID)
|
|
||||||
|
|
||||||
mh.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender, "as", intent.UserID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return members
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) ignoreEvent(evt *event.Event) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleMessage(evt *event.Event) {
|
|
||||||
if mh.ignoreEvent(evt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := mh.bridge.GetUserByMXID(evt.Sender)
|
|
||||||
if user == nil {
|
|
||||||
mh.log.Debugln("unknown user", evt.Sender)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content := evt.Content.AsMessage()
|
|
||||||
content.RemoveReplyFallback()
|
|
||||||
|
|
||||||
if content.MsgType == event.MsgText {
|
|
||||||
prefix := mh.bridge.Config.Bridge.CommandPrefix
|
|
||||||
|
|
||||||
hasPrefix := strings.HasPrefix(content.Body, prefix)
|
|
||||||
if hasPrefix {
|
|
||||||
content.Body = strings.TrimLeft(content.Body[len(prefix):], " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasPrefix || evt.RoomID == user.ManagementRoom {
|
|
||||||
mh.cmd.handle(evt.RoomID, user, content.Body, content.GetReplyTo())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
|
|
||||||
if portal != nil {
|
|
||||||
portal.matrixMessages <- portalMatrixMessage{user: user, evt: evt}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) int {
|
|
||||||
resp, err := intent.JoinRoomByID(evt.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Debugfln("Failed to join room %q as %q with invite from %q: %v", evt.RoomID, intent.UserID, evt.Sender, err)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
members, err := intent.Members(resp.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Debugfln("Failed to get members in room %q with invite from %q as %q: %v", resp.RoomID, evt.Sender, intent.UserID, err)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(members.Chunk) < 2 {
|
|
||||||
mh.log.Debugfln("Leaving empty room %q with invite from %q as %q", resp.RoomID, evt.Sender, intent.UserID)
|
|
||||||
|
|
||||||
intent.LeaveRoom(resp.RoomID)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(members.Chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) sendNoticeWithmarkdown(roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) {
|
|
||||||
intent := mh.as.BotIntent()
|
|
||||||
content := format.RenderMarkdown(message, true, false)
|
|
||||||
content.MsgType = event.MsgNotice
|
|
||||||
|
|
||||||
return intent.SendMessageEvent(roomID, event.EventMessage, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleBotInvite(evt *event.Event) {
|
|
||||||
intent := mh.as.BotIntent()
|
|
||||||
|
|
||||||
user := mh.bridge.GetUserByMXID(evt.Sender)
|
|
||||||
if user == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
members := mh.joinAndCheckMembers(evt, intent)
|
|
||||||
if members == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a DM and the user doesn't have a management room, make this
|
|
||||||
// the management room.
|
|
||||||
if members == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) {
|
|
||||||
user.SetManagementRoom(evt.RoomID)
|
|
||||||
|
|
||||||
intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room")
|
|
||||||
mh.log.Debugfln("%q registered as management room with %q", evt.RoomID, evt.Sender)
|
|
||||||
}
|
|
||||||
|
|
||||||
if evt.RoomID == user.ManagementRoom {
|
|
||||||
// Wait to send the welcome message until we're sure we're not in an empty
|
|
||||||
// room.
|
|
||||||
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Welcome)
|
|
||||||
|
|
||||||
if user.Connected() {
|
|
||||||
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Connected)
|
|
||||||
} else {
|
|
||||||
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.NotConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
additionalHelp := mh.bridge.Config.Bridge.ManagementRoomText.AdditionalHelp
|
|
||||||
if additionalHelp != "" {
|
|
||||||
mh.sendNoticeWithmarkdown(evt.RoomID, additionalHelp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
|
|
||||||
mh.log.Warnln("handling puppet invite!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleMembership(evt *event.Event) {
|
|
||||||
// Return early if we're supposed to ignore the event.
|
|
||||||
if mh.ignoreEvent(evt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if mh.bridge.crypto != nil {
|
|
||||||
mh.bridge.crypto.HandleMemberEvent(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the content of the event.
|
|
||||||
content := evt.Content.AsMember()
|
|
||||||
|
|
||||||
// Check if this is a new conversation from a matrix user to the bot
|
|
||||||
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mh.as.BotMXID() {
|
|
||||||
mh.handleBotInvite(evt)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load or create a new user.
|
|
||||||
user := mh.bridge.GetUserByMXID(evt.Sender)
|
|
||||||
if user == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
puppet := mh.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
|
|
||||||
|
|
||||||
// Load or create a new portal.
|
|
||||||
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
|
|
||||||
if portal == nil {
|
|
||||||
if content.Membership == event.MembershipInvite && puppet != nil {
|
|
||||||
mh.handlePuppetInvite(evt, user, puppet)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelf := id.UserID(evt.GetStateKey()) == evt.Sender
|
|
||||||
|
|
||||||
if content.Membership == event.MembershipLeave {
|
|
||||||
if evt.Unsigned.PrevContent != nil {
|
|
||||||
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
|
|
||||||
prevContent, ok := evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent)
|
|
||||||
if ok && prevContent.Membership != "join" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSelf {
|
|
||||||
portal.handleMatrixLeave(user)
|
|
||||||
} else if puppet != nil {
|
|
||||||
portal.handleMatrixKick(user, puppet)
|
|
||||||
}
|
|
||||||
} else if content.Membership == event.MembershipInvite {
|
|
||||||
portal.handleMatrixInvite(user, evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleReaction(evt *event.Event) {
|
|
||||||
if mh.ignoreEvent(evt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
|
|
||||||
if portal != nil {
|
|
||||||
portal.handleMatrixReaction(evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleRedaction(evt *event.Event) {
|
|
||||||
if mh.ignoreEvent(evt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
|
|
||||||
if portal != nil {
|
|
||||||
portal.handleMatrixRedaction(evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleEncryption(evt *event.Event) {
|
|
||||||
if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
|
|
||||||
if portal != nil && !portal.Encrypted {
|
|
||||||
mh.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID)
|
|
||||||
portal.Encrypted = true
|
|
||||||
portal.Update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionWaitTimeout = 5 * time.Second
|
|
||||||
|
|
||||||
func (mh *matrixHandler) handleEncrypted(evt *event.Event) {
|
|
||||||
if mh.ignoreEvent(evt) || mh.bridge.crypto == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypted, err := mh.bridge.crypto.Decrypt(evt)
|
|
||||||
decryptionRetryCount := 0
|
|
||||||
if errors.Is(err, NoSessionFound) {
|
|
||||||
content := evt.Content.AsEncrypted()
|
|
||||||
mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d seconds...", content.SessionID, evt.ID, int(sessionWaitTimeout.Seconds()))
|
|
||||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, false, decryptionRetryCount)
|
|
||||||
decryptionRetryCount++
|
|
||||||
|
|
||||||
if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, sessionWaitTimeout) {
|
|
||||||
mh.log.Debugfln("Got session %s after waiting, trying to decrypt %s again", content.SessionID, evt.ID)
|
|
||||||
decrypted, err = mh.bridge.crypto.Decrypt(evt)
|
|
||||||
} else {
|
|
||||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), false, decryptionRetryCount)
|
|
||||||
|
|
||||||
go mh.waitLongerForSession(evt)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, decryptionRetryCount)
|
|
||||||
|
|
||||||
mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err)
|
|
||||||
_, _ = mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf(
|
|
||||||
"\u26a0 Your message was not bridged: %v", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, decryptionRetryCount)
|
|
||||||
mh.bridge.eventProcessor.Dispatch(decrypted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *matrixHandler) waitLongerForSession(evt *event.Event) {
|
|
||||||
const extendedTimeout = sessionWaitTimeout * 3
|
|
||||||
|
|
||||||
content := evt.Content.AsEncrypted()
|
|
||||||
mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d more seconds...",
|
|
||||||
content.SessionID, evt.ID, int(extendedTimeout.Seconds()))
|
|
||||||
|
|
||||||
go mh.bridge.crypto.RequestSession(evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID)
|
|
||||||
|
|
||||||
resp, err := mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf(
|
|
||||||
"\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. "+
|
|
||||||
"The bridge will retry for %d seconds. If this error keeps happening, try restarting your client.",
|
|
||||||
int(extendedTimeout.Seconds())))
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Errorfln("Failed to send decryption error to %s: %v", evt.RoomID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
update := event.MessageEventContent{MsgType: event.MsgNotice}
|
|
||||||
|
|
||||||
if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, extendedTimeout) {
|
|
||||||
mh.log.Debugfln("Got session %s after waiting more, trying to decrypt %s again", content.SessionID, evt.ID)
|
|
||||||
|
|
||||||
decrypted, err := mh.bridge.crypto.Decrypt(evt)
|
|
||||||
if err == nil {
|
|
||||||
mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, 2)
|
|
||||||
mh.bridge.eventProcessor.Dispatch(decrypted)
|
|
||||||
_, _ = mh.bridge.bot.RedactEvent(evt.RoomID, resp.EventID)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err)
|
|
||||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, 2)
|
|
||||||
update.Body = fmt.Sprintf("\u26a0 Your message was not bridged: %v", err)
|
|
||||||
} else {
|
|
||||||
mh.log.Debugfln("Didn't get %s, giving up on %s", content.SessionID, evt.ID)
|
|
||||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), true, 2)
|
|
||||||
update.Body = "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. " +
|
|
||||||
"If this error keeps happening, try restarting your client."
|
|
||||||
}
|
|
||||||
|
|
||||||
newContent := update
|
|
||||||
update.NewContent = &newContent
|
|
||||||
if resp != nil {
|
|
||||||
update.RelatesTo = &event.RelatesTo{
|
|
||||||
Type: event.RelReplace,
|
|
||||||
EventID: resp.EventID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = mh.bridge.bot.SendMessageEvent(evt.RoomID, event.EventMessage, &update)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Debugfln("Failed to update decryption error notice %s: %v", resp.EventID, err)
|
|
||||||
}
|
|
||||||
}
|
|
1178
bridge/portal.go
1178
bridge/portal.go
File diff suppressed because it is too large
Load diff
291
bridge/puppet.go
291
bridge/puppet.go
|
@ -1,291 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Puppet struct {
|
|
||||||
*database.Puppet
|
|
||||||
|
|
||||||
bridge *Bridge
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
MXID id.UserID
|
|
||||||
|
|
||||||
customIntent *appservice.IntentAPI
|
|
||||||
customUser *User
|
|
||||||
|
|
||||||
syncLock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var userIDRegex *regexp.Regexp
|
|
||||||
|
|
||||||
func (b *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
|
|
||||||
return &Puppet{
|
|
||||||
Puppet: dbPuppet,
|
|
||||||
bridge: b,
|
|
||||||
log: b.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.ID)),
|
|
||||||
|
|
||||||
MXID: b.FormatPuppetMXID(dbPuppet.ID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) ParsePuppetMXID(mxid id.UserID) (string, bool) {
|
|
||||||
if userIDRegex == nil {
|
|
||||||
pattern := fmt.Sprintf(
|
|
||||||
"^@%s:%s$",
|
|
||||||
b.Config.Bridge.FormatUsername("([0-9]+)"),
|
|
||||||
b.Config.Homeserver.Domain,
|
|
||||||
)
|
|
||||||
|
|
||||||
userIDRegex = regexp.MustCompile(pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
match := userIDRegex.FindStringSubmatch(string(mxid))
|
|
||||||
if len(match) == 2 {
|
|
||||||
return match[1], true
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
|
|
||||||
id, ok := b.ParsePuppetMXID(mxid)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.GetPuppetByID(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetPuppetByID(id string) *Puppet {
|
|
||||||
b.puppetsLock.Lock()
|
|
||||||
defer b.puppetsLock.Unlock()
|
|
||||||
|
|
||||||
puppet, ok := b.puppets[id]
|
|
||||||
if !ok {
|
|
||||||
dbPuppet := b.db.Puppet.Get(id)
|
|
||||||
if dbPuppet == nil {
|
|
||||||
dbPuppet = b.db.Puppet.New()
|
|
||||||
dbPuppet.ID = id
|
|
||||||
dbPuppet.Insert()
|
|
||||||
}
|
|
||||||
|
|
||||||
puppet = b.NewPuppet(dbPuppet)
|
|
||||||
b.puppets[puppet.ID] = puppet
|
|
||||||
}
|
|
||||||
|
|
||||||
return puppet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet {
|
|
||||||
b.puppetsLock.Lock()
|
|
||||||
defer b.puppetsLock.Unlock()
|
|
||||||
|
|
||||||
puppet, ok := b.puppetsByCustomMXID[mxid]
|
|
||||||
if !ok {
|
|
||||||
dbPuppet := b.db.Puppet.GetByCustomMXID(mxid)
|
|
||||||
if dbPuppet == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
puppet = b.NewPuppet(dbPuppet)
|
|
||||||
b.puppets[puppet.ID] = puppet
|
|
||||||
b.puppetsByCustomMXID[puppet.CustomMXID] = puppet
|
|
||||||
}
|
|
||||||
|
|
||||||
return puppet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetAllPuppetsWithCustomMXID() []*Puppet {
|
|
||||||
return b.dbPuppetsToPuppets(b.db.Puppet.GetAllWithCustomMXID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetAllPuppets() []*Puppet {
|
|
||||||
return b.dbPuppetsToPuppets(b.db.Puppet.GetAll())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet {
|
|
||||||
b.puppetsLock.Lock()
|
|
||||||
defer b.puppetsLock.Unlock()
|
|
||||||
|
|
||||||
output := make([]*Puppet, len(dbPuppets))
|
|
||||||
for index, dbPuppet := range dbPuppets {
|
|
||||||
if dbPuppet == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
puppet, ok := b.puppets[dbPuppet.ID]
|
|
||||||
if !ok {
|
|
||||||
puppet = b.NewPuppet(dbPuppet)
|
|
||||||
b.puppets[dbPuppet.ID] = puppet
|
|
||||||
|
|
||||||
if dbPuppet.CustomMXID != "" {
|
|
||||||
b.puppetsByCustomMXID[dbPuppet.CustomMXID] = puppet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output[index] = puppet
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) FormatPuppetMXID(did string) id.UserID {
|
|
||||||
return id.NewUserID(
|
|
||||||
b.Config.Bridge.FormatUsername(did),
|
|
||||||
b.Config.Homeserver.Domain,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) DefaultIntent() *appservice.IntentAPI {
|
|
||||||
return p.bridge.as.Intent(p.MXID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
|
|
||||||
if p.customIntent == nil {
|
|
||||||
return p.DefaultIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.customIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) CustomIntent() *appservice.IntentAPI {
|
|
||||||
return p.customIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) updatePortalMeta(meta func(portal *Portal)) {
|
|
||||||
for _, portal := range p.bridge.GetAllPortalsByID(p.ID) {
|
|
||||||
// Get room create lock to prevent races between receiving contact info and room creation.
|
|
||||||
portal.roomCreateLock.Lock()
|
|
||||||
meta(portal)
|
|
||||||
portal.roomCreateLock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) updateName(source *User) bool {
|
|
||||||
user, err := source.Session.User(p.ID)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Warnln("failed to get user from id:", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
newName := p.bridge.Config.Bridge.FormatDisplayname(user)
|
|
||||||
|
|
||||||
if p.DisplayName != newName {
|
|
||||||
err := p.DefaultIntent().SetDisplayName(newName)
|
|
||||||
if err == nil {
|
|
||||||
p.DisplayName = newName
|
|
||||||
go p.updatePortalName()
|
|
||||||
p.Update()
|
|
||||||
} else {
|
|
||||||
p.log.Warnln("failed to set display name:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) updatePortalName() {
|
|
||||||
p.updatePortalMeta(func(portal *Portal) {
|
|
||||||
if portal.MXID != "" {
|
|
||||||
_, err := portal.MainIntent().SetRoomName(portal.MXID, p.DisplayName)
|
|
||||||
if err != nil {
|
|
||||||
portal.log.Warnln("Failed to set name:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.Name = p.DisplayName
|
|
||||||
portal.Update()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) updateAvatar(source *User) bool {
|
|
||||||
user, err := source.Session.User(p.ID)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Warnln("Failed to get user:", err)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Avatar == user.Avatar {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Avatar == "" {
|
|
||||||
p.log.Warnln("User does not have an avatar")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := uploadAvatar(p.DefaultIntent(), user.AvatarURL(""))
|
|
||||||
if err != nil {
|
|
||||||
p.log.Warnln("Failed to upload user avatar:", err)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
p.AvatarURL = url
|
|
||||||
|
|
||||||
err = p.DefaultIntent().SetAvatarURL(p.AvatarURL)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Warnln("Failed to set avatar:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.log.Debugln("Updated avatar", p.Avatar, "->", user.Avatar)
|
|
||||||
p.Avatar = user.Avatar
|
|
||||||
go p.updatePortalAvatar()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) updatePortalAvatar() {
|
|
||||||
p.updatePortalMeta(func(portal *Portal) {
|
|
||||||
if portal.MXID != "" {
|
|
||||||
_, err := portal.MainIntent().SetRoomAvatar(portal.MXID, p.AvatarURL)
|
|
||||||
if err != nil {
|
|
||||||
portal.log.Warnln("Failed to set avatar:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.AvatarURL = p.AvatarURL
|
|
||||||
portal.Avatar = p.Avatar
|
|
||||||
portal.Update()
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) SyncContact(source *User) {
|
|
||||||
p.syncLock.Lock()
|
|
||||||
defer p.syncLock.Unlock()
|
|
||||||
|
|
||||||
p.log.Debugln("syncing contact", p.DisplayName)
|
|
||||||
|
|
||||||
err := p.DefaultIntent().EnsureRegistered()
|
|
||||||
if err != nil {
|
|
||||||
p.log.Errorln("Failed to ensure registered:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
update := false
|
|
||||||
|
|
||||||
update = p.updateName(source) || update
|
|
||||||
|
|
||||||
if p.Avatar == "" {
|
|
||||||
update = p.updateAvatar(source) || update
|
|
||||||
p.log.Debugln("update avatar returned", update)
|
|
||||||
}
|
|
||||||
|
|
||||||
if update {
|
|
||||||
p.Update()
|
|
||||||
}
|
|
||||||
}
|
|
826
bridge/user.go
826
bridge/user.go
|
@ -1,826 +0,0 @@
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
|
||||||
"github.com/skip2/go-qrcode"
|
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotConnected = errors.New("not connected")
|
|
||||||
ErrNotLoggedIn = errors.New("not logged in")
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
*database.User
|
|
||||||
|
|
||||||
sync.Mutex
|
|
||||||
|
|
||||||
bridge *Bridge
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
// TODO finish implementing
|
|
||||||
Admin bool
|
|
||||||
|
|
||||||
guilds map[string]*database.Guild
|
|
||||||
guildsLock sync.Mutex
|
|
||||||
|
|
||||||
Session *discordgo.Session
|
|
||||||
}
|
|
||||||
|
|
||||||
// this assume you are holding the guilds lock!!!
|
|
||||||
func (u *User) loadGuilds() {
|
|
||||||
u.guilds = map[string]*database.Guild{}
|
|
||||||
for _, guild := range u.bridge.db.Guild.GetAll(u.ID) {
|
|
||||||
u.guilds[guild.GuildID] = guild
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) loadUser(dbUser *database.User, mxid *id.UserID) *User {
|
|
||||||
// If we weren't passed in a user we attempt to create one if we were given
|
|
||||||
// a matrix id.
|
|
||||||
if dbUser == nil {
|
|
||||||
if mxid == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dbUser = b.db.User.New()
|
|
||||||
dbUser.MXID = *mxid
|
|
||||||
dbUser.Insert()
|
|
||||||
}
|
|
||||||
|
|
||||||
user := b.NewUser(dbUser)
|
|
||||||
|
|
||||||
// We assume the usersLock was acquired by our caller.
|
|
||||||
b.usersByMXID[user.MXID] = user
|
|
||||||
if user.ID != "" {
|
|
||||||
b.usersByID[user.ID] = user
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.ManagementRoom != "" {
|
|
||||||
// Lock the management rooms for our update
|
|
||||||
b.managementRoomsLock.Lock()
|
|
||||||
b.managementRooms[user.ManagementRoom] = user
|
|
||||||
b.managementRoomsLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load our guilds state from the database and turn it into a map
|
|
||||||
user.guildsLock.Lock()
|
|
||||||
user.loadGuilds()
|
|
||||||
user.guildsLock.Unlock()
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetUserByMXID(userID id.UserID) *User {
|
|
||||||
// TODO: check if puppet
|
|
||||||
|
|
||||||
b.usersLock.Lock()
|
|
||||||
defer b.usersLock.Unlock()
|
|
||||||
|
|
||||||
user, ok := b.usersByMXID[userID]
|
|
||||||
if !ok {
|
|
||||||
return b.loadUser(b.db.User.GetByMXID(userID), &userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) GetUserByID(id string) *User {
|
|
||||||
b.usersLock.Lock()
|
|
||||||
defer b.usersLock.Unlock()
|
|
||||||
|
|
||||||
user, ok := b.usersByID[id]
|
|
||||||
if !ok {
|
|
||||||
return b.loadUser(b.db.User.GetByID(id), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) NewUser(dbUser *database.User) *User {
|
|
||||||
user := &User{
|
|
||||||
User: dbUser,
|
|
||||||
bridge: b,
|
|
||||||
log: b.log.Sub("User").Sub(string(dbUser.MXID)),
|
|
||||||
guilds: map[string]*database.Guild{},
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) getAllUsers() []*User {
|
|
||||||
b.usersLock.Lock()
|
|
||||||
defer b.usersLock.Unlock()
|
|
||||||
|
|
||||||
dbUsers := b.db.User.GetAll()
|
|
||||||
users := make([]*User, len(dbUsers))
|
|
||||||
|
|
||||||
for idx, dbUser := range dbUsers {
|
|
||||||
user, ok := b.usersByMXID[dbUser.MXID]
|
|
||||||
if !ok {
|
|
||||||
user = b.loadUser(dbUser, nil)
|
|
||||||
}
|
|
||||||
users[idx] = user
|
|
||||||
}
|
|
||||||
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) startUsers() {
|
|
||||||
b.log.Debugln("Starting users")
|
|
||||||
|
|
||||||
for _, user := range b.getAllUsers() {
|
|
||||||
go user.Connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.log.Debugln("Starting custom puppets")
|
|
||||||
for _, customPuppet := range b.GetAllPuppetsWithCustomMXID() {
|
|
||||||
go func(puppet *Puppet) {
|
|
||||||
b.log.Debugln("Starting custom puppet", puppet.CustomMXID)
|
|
||||||
|
|
||||||
if err := puppet.StartCustomMXID(true); err != nil {
|
|
||||||
puppet.log.Errorln("Failed to start custom puppet:", err)
|
|
||||||
}
|
|
||||||
}(customPuppet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) SetManagementRoom(roomID id.RoomID) {
|
|
||||||
u.bridge.managementRoomsLock.Lock()
|
|
||||||
defer u.bridge.managementRoomsLock.Unlock()
|
|
||||||
|
|
||||||
existing, ok := u.bridge.managementRooms[roomID]
|
|
||||||
if ok {
|
|
||||||
// If there's a user already assigned to this management room, clear it
|
|
||||||
// out.
|
|
||||||
// I think this is due a name change or something? I dunno, leaving it
|
|
||||||
// for now.
|
|
||||||
existing.ManagementRoom = ""
|
|
||||||
existing.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
u.ManagementRoom = roomID
|
|
||||||
u.bridge.managementRooms[u.ManagementRoom] = u
|
|
||||||
u.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) sendQRCode(bot *appservice.IntentAPI, roomID id.RoomID, code string) (id.EventID, error) {
|
|
||||||
url, err := u.uploadQRCode(code)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
content := event.MessageEventContent{
|
|
||||||
MsgType: event.MsgImage,
|
|
||||||
Body: code,
|
|
||||||
URL: url.CUString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := bot.SendMessageEvent(roomID, event.EventMessage, &content)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.EventID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) uploadQRCode(code string) (id.ContentURI, error) {
|
|
||||||
qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
|
|
||||||
if err != nil {
|
|
||||||
u.log.Errorln("Failed to encode QR code:", err)
|
|
||||||
|
|
||||||
return id.ContentURI{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bot := u.bridge.as.BotClient()
|
|
||||||
|
|
||||||
resp, err := bot.UploadBytes(qrCode, "image/png")
|
|
||||||
if err != nil {
|
|
||||||
u.log.Errorln("Failed to upload QR code:", err)
|
|
||||||
|
|
||||||
return id.ContentURI{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.ContentURI, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) tryAutomaticDoublePuppeting() {
|
|
||||||
u.Lock()
|
|
||||||
defer u.Unlock()
|
|
||||||
|
|
||||||
if !u.bridge.Config.CanAutoDoublePuppet(u.MXID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log.Debugln("Checking if double puppeting needs to be enabled")
|
|
||||||
|
|
||||||
puppet := u.bridge.GetPuppetByID(u.ID)
|
|
||||||
if puppet.CustomMXID != "" {
|
|
||||||
u.log.Debugln("User already has double-puppeting enabled")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, err := puppet.loginWithSharedSecret(u.MXID)
|
|
||||||
if err != nil {
|
|
||||||
u.log.Warnln("Failed to login with shared secret:", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = puppet.SwitchCustomMXID(accessToken, u.MXID)
|
|
||||||
if err != nil {
|
|
||||||
puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log.Infoln("Successfully automatically enabled custom puppet")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
|
|
||||||
doublePuppet := portal.bridge.GetPuppetByCustomMXID(u.MXID)
|
|
||||||
if doublePuppet == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if doublePuppet == nil || doublePuppet.CustomIntent() == nil || portal.MXID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO sync mute status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Login(token string) error {
|
|
||||||
if token == "" {
|
|
||||||
return fmt.Errorf("No token specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Token = token
|
|
||||||
u.Update()
|
|
||||||
|
|
||||||
return u.Connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) LoggedIn() bool {
|
|
||||||
u.Lock()
|
|
||||||
defer u.Unlock()
|
|
||||||
|
|
||||||
return u.Token != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Logout() error {
|
|
||||||
u.Lock()
|
|
||||||
defer u.Unlock()
|
|
||||||
|
|
||||||
if u.Session == nil {
|
|
||||||
return ErrNotLoggedIn
|
|
||||||
}
|
|
||||||
|
|
||||||
puppet := u.bridge.GetPuppetByID(u.ID)
|
|
||||||
if puppet.CustomMXID != "" {
|
|
||||||
err := puppet.SwitchCustomMXID("", "")
|
|
||||||
if err != nil {
|
|
||||||
u.log.Warnln("Failed to logout-matrix while logging out of Discord:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.Session.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Session = nil
|
|
||||||
|
|
||||||
u.Token = ""
|
|
||||||
u.Update()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Connected() bool {
|
|
||||||
u.Lock()
|
|
||||||
defer u.Unlock()
|
|
||||||
|
|
||||||
return u.Session != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Connect() error {
|
|
||||||
u.Lock()
|
|
||||||
defer u.Unlock()
|
|
||||||
|
|
||||||
if u.Token == "" {
|
|
||||||
return ErrNotLoggedIn
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log.Debugln("connecting to discord")
|
|
||||||
|
|
||||||
session, err := discordgo.New(u.Token)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Session = session
|
|
||||||
|
|
||||||
// Add our event handlers
|
|
||||||
u.Session.AddHandler(u.readyHandler)
|
|
||||||
u.Session.AddHandler(u.connectedHandler)
|
|
||||||
u.Session.AddHandler(u.disconnectedHandler)
|
|
||||||
|
|
||||||
u.Session.AddHandler(u.guildCreateHandler)
|
|
||||||
u.Session.AddHandler(u.guildDeleteHandler)
|
|
||||||
u.Session.AddHandler(u.guildUpdateHandler)
|
|
||||||
|
|
||||||
u.Session.AddHandler(u.channelCreateHandler)
|
|
||||||
u.Session.AddHandler(u.channelDeleteHandler)
|
|
||||||
u.Session.AddHandler(u.channelPinsUpdateHandler)
|
|
||||||
u.Session.AddHandler(u.channelUpdateHandler)
|
|
||||||
|
|
||||||
u.Session.AddHandler(u.messageCreateHandler)
|
|
||||||
u.Session.AddHandler(u.messageDeleteHandler)
|
|
||||||
u.Session.AddHandler(u.messageUpdateHandler)
|
|
||||||
u.Session.AddHandler(u.reactionAddHandler)
|
|
||||||
u.Session.AddHandler(u.reactionRemoveHandler)
|
|
||||||
|
|
||||||
u.Session.Identify.Presence.Status = "online"
|
|
||||||
|
|
||||||
return u.Session.Open()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Disconnect() error {
|
|
||||||
u.Lock()
|
|
||||||
defer u.Unlock()
|
|
||||||
|
|
||||||
if u.Session == nil {
|
|
||||||
return ErrNotConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.Session.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Session = nil
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) bridgeMessage(guildID string) bool {
|
|
||||||
// Non guild message always get bridged.
|
|
||||||
if guildID == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
if guild, found := u.guilds[guildID]; found {
|
|
||||||
if guild.Bridge {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log.Debugfln("ignoring message for non-bridged guild %s-%s", u.ID, guildID)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) readyHandler(s *discordgo.Session, r *discordgo.Ready) {
|
|
||||||
u.log.Debugln("discord connection ready")
|
|
||||||
|
|
||||||
// Update our user fields
|
|
||||||
u.ID = r.User.ID
|
|
||||||
|
|
||||||
// Update our guild map to match watch discord thinks we're in. This is the
|
|
||||||
// only time we can get the full guild map as discordgo doesn't make it
|
|
||||||
// available to us later. Also, discord might not give us the full guild
|
|
||||||
// information here, so we use this to remove guilds the user left and only
|
|
||||||
// add guilds whose full information we have. The are told about the
|
|
||||||
// "unavailable" guilds later via the GuildCreate handler.
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
// build a list of the current guilds we're in so we can prune the old ones
|
|
||||||
current := []string{}
|
|
||||||
|
|
||||||
u.log.Debugln("database guild count", len(u.guilds))
|
|
||||||
u.log.Debugln("discord guild count", len(r.Guilds))
|
|
||||||
|
|
||||||
for _, guild := range r.Guilds {
|
|
||||||
current = append(current, guild.ID)
|
|
||||||
|
|
||||||
// If we already know about this guild, make sure we reset it's bridge
|
|
||||||
// status.
|
|
||||||
if val, found := u.guilds[guild.ID]; found {
|
|
||||||
bridge := val.Bridge
|
|
||||||
u.guilds[guild.ID].Bridge = bridge
|
|
||||||
|
|
||||||
// Update the name if the guild is available
|
|
||||||
if !guild.Unavailable {
|
|
||||||
u.guilds[guild.ID].GuildName = guild.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
val.Upsert()
|
|
||||||
} else {
|
|
||||||
g := u.bridge.db.Guild.New()
|
|
||||||
g.DiscordID = u.ID
|
|
||||||
g.GuildID = guild.ID
|
|
||||||
u.guilds[guild.ID] = g
|
|
||||||
|
|
||||||
if !guild.Unavailable {
|
|
||||||
g.GuildName = guild.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Upsert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync the guilds to the database.
|
|
||||||
u.bridge.db.Guild.Prune(u.ID, current)
|
|
||||||
|
|
||||||
// Finally reload from the database since it purged servers we're not in
|
|
||||||
// anymore.
|
|
||||||
u.loadGuilds()
|
|
||||||
|
|
||||||
u.log.Debugln("updated database guild count", len(u.guilds))
|
|
||||||
|
|
||||||
u.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) connectedHandler(s *discordgo.Session, c *discordgo.Connect) {
|
|
||||||
u.log.Debugln("connected to discord")
|
|
||||||
|
|
||||||
u.tryAutomaticDoublePuppeting()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) disconnectedHandler(s *discordgo.Session, d *discordgo.Disconnect) {
|
|
||||||
u.log.Debugln("disconnected from discord")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) guildCreateHandler(s *discordgo.Session, g *discordgo.GuildCreate) {
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
// If we somehow already know about the guild, just update it's name
|
|
||||||
if guild, found := u.guilds[g.ID]; found {
|
|
||||||
guild.GuildName = g.Name
|
|
||||||
guild.Upsert()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a brand new guild so lets get it added.
|
|
||||||
guild := u.bridge.db.Guild.New()
|
|
||||||
guild.DiscordID = u.ID
|
|
||||||
guild.GuildID = g.ID
|
|
||||||
guild.GuildName = g.Name
|
|
||||||
guild.Upsert()
|
|
||||||
|
|
||||||
u.guilds[g.ID] = guild
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) guildDeleteHandler(s *discordgo.Session, g *discordgo.GuildDelete) {
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
if guild, found := u.guilds[g.ID]; found {
|
|
||||||
guild.Delete()
|
|
||||||
delete(u.guilds, g.ID)
|
|
||||||
u.log.Debugln("deleted guild", g.Guild.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) guildUpdateHandler(s *discordgo.Session, g *discordgo.GuildUpdate) {
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
// If we somehow already know about the guild, just update it's name
|
|
||||||
if guild, found := u.guilds[g.ID]; found {
|
|
||||||
guild.GuildName = g.Name
|
|
||||||
guild.Upsert()
|
|
||||||
|
|
||||||
u.log.Debugln("updated guild", g.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) createChannel(c *discordgo.Channel) {
|
|
||||||
key := database.NewPortalKey(c.ID, u.User.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
if portal.MXID != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.Name = c.Name
|
|
||||||
portal.Topic = c.Topic
|
|
||||||
portal.Type = c.Type
|
|
||||||
|
|
||||||
if portal.Type == discordgo.ChannelTypeDM {
|
|
||||||
portal.DMUser = c.Recipients[0].ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Icon != "" {
|
|
||||||
u.log.Debugln("channel icon", c.Icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.Update()
|
|
||||||
|
|
||||||
portal.createMatrixRoom(u, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) channelCreateHandler(s *discordgo.Session, c *discordgo.ChannelCreate) {
|
|
||||||
u.createChannel(c.Channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) channelDeleteHandler(s *discordgo.Session, c *discordgo.ChannelDelete) {
|
|
||||||
u.log.Debugln("channel delete handler")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) channelPinsUpdateHandler(s *discordgo.Session, c *discordgo.ChannelPinsUpdate) {
|
|
||||||
u.log.Debugln("channel pins update")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) channelUpdateHandler(s *discordgo.Session, c *discordgo.ChannelUpdate) {
|
|
||||||
key := database.NewPortalKey(c.ID, u.User.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
portal.update(u, c.Channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) messageCreateHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
||||||
if !u.bridgeMessage(m.GuildID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := database.NewPortalKey(m.ChannelID, u.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
msg := portalDiscordMessage{
|
|
||||||
msg: m,
|
|
||||||
user: u,
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.discordMessages <- msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) messageDeleteHandler(s *discordgo.Session, m *discordgo.MessageDelete) {
|
|
||||||
if !u.bridgeMessage(m.GuildID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := database.NewPortalKey(m.ChannelID, u.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
msg := portalDiscordMessage{
|
|
||||||
msg: m,
|
|
||||||
user: u,
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.discordMessages <- msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) messageUpdateHandler(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
|
||||||
if !u.bridgeMessage(m.GuildID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := database.NewPortalKey(m.ChannelID, u.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
msg := portalDiscordMessage{
|
|
||||||
msg: m,
|
|
||||||
user: u,
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.discordMessages <- msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) reactionAddHandler(s *discordgo.Session, m *discordgo.MessageReactionAdd) {
|
|
||||||
if !u.bridgeMessage(m.MessageReaction.GuildID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := database.NewPortalKey(m.ChannelID, u.User.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
msg := portalDiscordMessage{
|
|
||||||
msg: m,
|
|
||||||
user: u,
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.discordMessages <- msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) reactionRemoveHandler(s *discordgo.Session, m *discordgo.MessageReactionRemove) {
|
|
||||||
if !u.bridgeMessage(m.MessageReaction.GuildID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := database.NewPortalKey(m.ChannelID, u.User.ID)
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
|
|
||||||
msg := portalDiscordMessage{
|
|
||||||
msg: m,
|
|
||||||
user: u,
|
|
||||||
}
|
|
||||||
|
|
||||||
portal.discordMessages <- msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) bool {
|
|
||||||
ret := false
|
|
||||||
|
|
||||||
inviteContent := event.Content{
|
|
||||||
Parsed: &event.MemberEventContent{
|
|
||||||
Membership: event.MembershipInvite,
|
|
||||||
IsDirect: isDirect,
|
|
||||||
},
|
|
||||||
Raw: map[string]interface{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
customPuppet := u.bridge.GetPuppetByCustomMXID(u.MXID)
|
|
||||||
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
|
||||||
inviteContent.Raw["fi.mau.will_auto_accept"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := intent.SendStateEvent(roomID, event.StateMember, u.MXID.String(), &inviteContent)
|
|
||||||
|
|
||||||
var httpErr mautrix.HTTPError
|
|
||||||
if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
|
|
||||||
u.bridge.StateStore.SetMembership(roomID, u.MXID, event.MembershipJoin)
|
|
||||||
ret = true
|
|
||||||
} else if err != nil {
|
|
||||||
u.log.Warnfln("Failed to invite user to %s: %v", roomID, err)
|
|
||||||
} else {
|
|
||||||
ret = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
|
||||||
err = customPuppet.CustomIntent().EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
|
|
||||||
if err != nil {
|
|
||||||
u.log.Warnfln("Failed to auto-join %s: %v", roomID, err)
|
|
||||||
ret = false
|
|
||||||
} else {
|
|
||||||
ret = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) getDirectChats() map[id.UserID][]id.RoomID {
|
|
||||||
chats := map[id.UserID][]id.RoomID{}
|
|
||||||
|
|
||||||
privateChats := u.bridge.db.Portal.FindPrivateChats(u.ID)
|
|
||||||
for _, portal := range privateChats {
|
|
||||||
if portal.MXID != "" {
|
|
||||||
puppetMXID := u.bridge.FormatPuppetMXID(portal.Key.Receiver)
|
|
||||||
|
|
||||||
chats[puppetMXID] = []id.RoomID{portal.MXID}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) updateDirectChats(chats map[id.UserID][]id.RoomID) {
|
|
||||||
if !u.bridge.Config.Bridge.SyncDirectChatList {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
puppet := u.bridge.GetPuppetByMXID(u.MXID)
|
|
||||||
if puppet == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
intent := puppet.CustomIntent()
|
|
||||||
if intent == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
method := http.MethodPatch
|
|
||||||
if chats == nil {
|
|
||||||
chats = u.getDirectChats()
|
|
||||||
method = http.MethodPut
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log.Debugln("Updating m.direct list on homeserver")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if u.bridge.Config.Homeserver.Asmux {
|
|
||||||
urlPath := intent.BuildURL(mautrix.ClientURLPath{"unstable", "com.beeper.asmux", "dms"})
|
|
||||||
_, err = intent.MakeFullRequest(mautrix.FullRequest{
|
|
||||||
Method: method,
|
|
||||||
URL: urlPath,
|
|
||||||
Headers: http.Header{"X-Asmux-Auth": {u.bridge.as.Registration.AppToken}},
|
|
||||||
RequestJSON: chats,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
existingChats := map[id.UserID][]id.RoomID{}
|
|
||||||
|
|
||||||
err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats)
|
|
||||||
if err != nil {
|
|
||||||
u.log.Warnln("Failed to get m.direct list to update it:", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for userID, rooms := range existingChats {
|
|
||||||
if _, ok := u.bridge.ParsePuppetMXID(userID); !ok {
|
|
||||||
// This is not a ghost user, include it in the new list
|
|
||||||
chats[userID] = rooms
|
|
||||||
} else if _, ok := chats[userID]; !ok && method == http.MethodPatch {
|
|
||||||
// This is a ghost user, but we're not replacing the whole list, so include it too
|
|
||||||
chats[userID] = rooms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
u.log.Warnln("Failed to update m.direct list:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) bridgeGuild(guildID string, everything bool) error {
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
guild, found := u.guilds[guildID]
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("guildID not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the guild
|
|
||||||
guild.Bridge = true
|
|
||||||
guild.Upsert()
|
|
||||||
|
|
||||||
// If this is a full bridge, create portals for all the channels
|
|
||||||
if everything {
|
|
||||||
channels, err := u.Session.GuildChannels(guildID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channel := range channels {
|
|
||||||
if channelIsBridgeable(channel) {
|
|
||||||
u.createChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) unbridgeGuild(guildID string) error {
|
|
||||||
u.guildsLock.Lock()
|
|
||||||
defer u.guildsLock.Unlock()
|
|
||||||
|
|
||||||
guild, exists := u.guilds[guildID]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("guildID not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !guild.Bridge {
|
|
||||||
return fmt.Errorf("guild not bridged")
|
|
||||||
}
|
|
||||||
|
|
||||||
// First update the guild so we don't have any other go routines recreating
|
|
||||||
// channels we're about to destroy.
|
|
||||||
guild.Bridge = false
|
|
||||||
guild.Upsert()
|
|
||||||
|
|
||||||
// Now run through the channels in the guild and remove any portals we
|
|
||||||
// have for them.
|
|
||||||
channels, err := u.Session.GuildChannels(guildID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channel := range channels {
|
|
||||||
if channelIsBridgeable(channel) {
|
|
||||||
key := database.PortalKey{
|
|
||||||
ChannelID: channel.ID,
|
|
||||||
Receiver: u.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
portal := u.bridge.GetPortalByID(key)
|
|
||||||
portal.leave(u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
2
build.sh
Executable file
2
build.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
go build -ldflags "-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date '+%b %_d %Y, %H:%M:%S'`'" "$@"
|
285
commands.go
Normal file
285
commands.go
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// Copyright (C) 2022 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-discord/remoteauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WrappedCommandEvent struct {
|
||||||
|
*commands.Event
|
||||||
|
Bridge *DiscordBridge
|
||||||
|
User *User
|
||||||
|
Portal *Portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) RegisterCommands() {
|
||||||
|
proc := br.CommandProcessor.(*commands.Processor)
|
||||||
|
proc.AddHandlers(
|
||||||
|
cmdLogin,
|
||||||
|
cmdLogout,
|
||||||
|
cmdReconnect,
|
||||||
|
cmdDisconnect,
|
||||||
|
cmdGuilds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapCommand(handler func(*WrappedCommandEvent)) func(*commands.Event) {
|
||||||
|
return func(ce *commands.Event) {
|
||||||
|
user := ce.User.(*User)
|
||||||
|
var portal *Portal
|
||||||
|
if ce.Portal != nil {
|
||||||
|
portal = ce.Portal.(*Portal)
|
||||||
|
}
|
||||||
|
br := ce.Bridge.Child.(*DiscordBridge)
|
||||||
|
handler(&WrappedCommandEvent{ce, br, user, portal})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdLogin = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnLogin),
|
||||||
|
Name: "login",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Link the bridge to your Discord account by scanning a QR code.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnLogin(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.IsLoggedIn() {
|
||||||
|
ce.Reply("You're already logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := remoteauth.New()
|
||||||
|
if err != nil {
|
||||||
|
ce.Reply("Failed to prepare login: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qrChan := make(chan string)
|
||||||
|
doneChan := make(chan struct{})
|
||||||
|
|
||||||
|
var qrCodeEvent id.EventID
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
code := <-qrChan
|
||||||
|
resp := sendQRCode(ce, code)
|
||||||
|
qrCodeEvent = resp
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err = client.Dial(ctx, qrChan, doneChan); err != nil {
|
||||||
|
close(qrChan)
|
||||||
|
close(doneChan)
|
||||||
|
ce.Reply("Error connecting to login websocket: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
<-doneChan
|
||||||
|
|
||||||
|
if qrCodeEvent != "" {
|
||||||
|
_, _ = ce.MainIntent().RedactEvent(ce.RoomID, qrCodeEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := client.Result()
|
||||||
|
if err != nil || len(user.Token) == 0 {
|
||||||
|
ce.Reply("Error logging in: %v", err)
|
||||||
|
} else if err = ce.User.Login(user.Token); err != nil {
|
||||||
|
ce.Reply("Error connecting after login: %v", err)
|
||||||
|
}
|
||||||
|
ce.User.Lock()
|
||||||
|
ce.User.ID = user.UserID
|
||||||
|
ce.User.Update()
|
||||||
|
ce.User.Unlock()
|
||||||
|
ce.Reply("Successfully logged in as %s#%s", user.Username, user.Discriminator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendQRCode(ce *WrappedCommandEvent, code string) id.EventID {
|
||||||
|
url, ok := uploadQRCode(ce, code)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
content := event.MessageEventContent{
|
||||||
|
MsgType: event.MsgImage,
|
||||||
|
Body: code,
|
||||||
|
URL: url.CUString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content)
|
||||||
|
if err != nil {
|
||||||
|
ce.Log.Errorfln("Failed to send QR code: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.EventID
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadQRCode(ce *WrappedCommandEvent, code string) (id.ContentURI, bool) {
|
||||||
|
qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
|
||||||
|
if err != nil {
|
||||||
|
ce.Log.Errorln("Failed to encode QR code:", err)
|
||||||
|
ce.Reply("Failed to encode QR code: %v", err)
|
||||||
|
return id.ContentURI{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ce.Bot.UploadBytes(qrCode, "image/png")
|
||||||
|
if err != nil {
|
||||||
|
ce.Log.Errorln("Failed to upload QR code:", err)
|
||||||
|
ce.Reply("Failed to upload QR code: %v", err)
|
||||||
|
return id.ContentURI{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.ContentURI, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdLogout = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnLogout),
|
||||||
|
Name: "logout",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Unlink the bridge from your WhatsApp account.",
|
||||||
|
},
|
||||||
|
RequiresLogin: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnLogout(ce *WrappedCommandEvent) {
|
||||||
|
err := ce.User.Logout()
|
||||||
|
if err != nil {
|
||||||
|
ce.Reply("Error logging out: %v", err)
|
||||||
|
} else {
|
||||||
|
ce.Reply("Logged out successfully.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDisconnect = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnDisconnect),
|
||||||
|
Name: "disconnect",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Disconnect from Discord (without logging out)",
|
||||||
|
},
|
||||||
|
RequiresLogin: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnDisconnect(ce *WrappedCommandEvent) {
|
||||||
|
if !ce.User.Connected() {
|
||||||
|
ce.Reply("You're already not connected")
|
||||||
|
} else if err := ce.User.Disconnect(); err != nil {
|
||||||
|
ce.Reply("Error while disconnecting: %v", err)
|
||||||
|
} else {
|
||||||
|
ce.Reply("Successfully disconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdReconnect = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnReconnect),
|
||||||
|
Name: "reconnect",
|
||||||
|
Aliases: []string{"connect"},
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Reconnect to Discord after disconnecting",
|
||||||
|
},
|
||||||
|
RequiresLogin: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnReconnect(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Connected() {
|
||||||
|
ce.Reply("You're already connected")
|
||||||
|
} else if err := ce.User.Connect(); err != nil {
|
||||||
|
ce.Reply("Error while reconnecting: %v", err)
|
||||||
|
} else {
|
||||||
|
ce.Reply("Successfully reconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdGuilds = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnGuilds),
|
||||||
|
Name: "guilds",
|
||||||
|
Aliases: []string{"servers", "guild", "server"},
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionUnclassified,
|
||||||
|
Description: "Guild bridging management",
|
||||||
|
Args: "<status/bridge/unbridge> [_guild ID_] [--entire]",
|
||||||
|
},
|
||||||
|
RequiresLogin: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnGuilds(ce *WrappedCommandEvent) {
|
||||||
|
if len(ce.Args) == 0 {
|
||||||
|
ce.Reply("**Usage**: `$cmdprefix guilds <status/bridge/unbridge> [guild ID] [--entire]`")
|
||||||
|
}
|
||||||
|
subcommand := strings.ToLower(ce.Args[0])
|
||||||
|
ce.Args = ce.Args[1:]
|
||||||
|
switch subcommand {
|
||||||
|
case "status":
|
||||||
|
fnListGuilds(ce)
|
||||||
|
case "bridge":
|
||||||
|
fnBridgeGuild(ce)
|
||||||
|
case "unbridge":
|
||||||
|
fnUnbridgeGuild(ce)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnListGuilds(ce *WrappedCommandEvent) {
|
||||||
|
ce.User.guildsLock.Lock()
|
||||||
|
defer ce.User.guildsLock.Unlock()
|
||||||
|
if len(ce.User.guilds) == 0 {
|
||||||
|
ce.Reply("You haven't joined any guilds")
|
||||||
|
} else {
|
||||||
|
var output strings.Builder
|
||||||
|
for _, guild := range ce.User.guilds {
|
||||||
|
status := "not bridged"
|
||||||
|
if guild.Bridge {
|
||||||
|
status = "bridged"
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(&output, "* %s (`%s`) - %s\n", guild.GuildName, guild.GuildID, status)
|
||||||
|
}
|
||||||
|
ce.Reply("List of guilds:\n\n%s", output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnBridgeGuild(ce *WrappedCommandEvent) {
|
||||||
|
if len(ce.Args) == 0 || len(ce.Args) > 2 {
|
||||||
|
ce.Reply("**Usage**: `$cmdprefix guilds bridge <guild ID> [--entire]")
|
||||||
|
} else if err := ce.User.bridgeGuild(ce.Args[0], len(ce.Args) == 2 && strings.ToLower(ce.Args[1]) == "--entire"); err != nil {
|
||||||
|
ce.Reply("Error bridging guild: %v", err)
|
||||||
|
} else {
|
||||||
|
ce.Reply("Successfully bridged guild")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func fnUnbridgeGuild(ce *WrappedCommandEvent) {
|
||||||
|
if len(ce.Args) != 1 {
|
||||||
|
ce.Reply("**Usage**: `$cmdprefix guilds unbridge <guild ID>")
|
||||||
|
} else if err := ce.User.unbridgeGuild(ce.Args[0]); err != nil {
|
||||||
|
ce.Reply("Error unbridging guild: %v", err)
|
||||||
|
} else {
|
||||||
|
ce.Reply("Successfully unbridged guild")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,85 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
as "maunium.net/go/mautrix/appservice"
|
|
||||||
)
|
|
||||||
|
|
||||||
type appservice struct {
|
|
||||||
Address string `yaml:"address"`
|
|
||||||
Hostname string `yaml:"hostname"`
|
|
||||||
Port uint16 `yaml:"port"`
|
|
||||||
|
|
||||||
ID string `yaml:"id"`
|
|
||||||
|
|
||||||
Bot bot `yaml:"bot"`
|
|
||||||
|
|
||||||
Provisioning provisioning `yaml:"provisioning"`
|
|
||||||
|
|
||||||
Database database `yaml:"database"`
|
|
||||||
|
|
||||||
EphemeralEvents bool `yaml:"ephemeral_events"`
|
|
||||||
|
|
||||||
ASToken string `yaml:"as_token"`
|
|
||||||
HSToken string `yaml:"hs_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *appservice) validate() error {
|
|
||||||
if a.ID == "" {
|
|
||||||
a.ID = "discord"
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Address == "" {
|
|
||||||
a.Address = "http://localhost:29350"
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Hostname == "" {
|
|
||||||
a.Hostname = "0.0.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Port == 0 {
|
|
||||||
a.Port = 29350
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.Database.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.Bot.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *appservice) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawAppservice appservice
|
|
||||||
|
|
||||||
raw := rawAppservice{}
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*a = appservice(raw)
|
|
||||||
|
|
||||||
return a.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) CreateAppService() (*as.AppService, error) {
|
|
||||||
appservice := as.Create()
|
|
||||||
|
|
||||||
appservice.HomeserverURL = cfg.Homeserver.Address
|
|
||||||
appservice.HomeserverDomain = cfg.Homeserver.Domain
|
|
||||||
|
|
||||||
appservice.Host.Hostname = cfg.Appservice.Hostname
|
|
||||||
appservice.Host.Port = cfg.Appservice.Port
|
|
||||||
appservice.DefaultHTTPRetries = 4
|
|
||||||
|
|
||||||
reg, err := cfg.getRegistration()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
appservice.Registration = reg
|
|
||||||
|
|
||||||
return appservice, nil
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
type bot struct {
|
|
||||||
Username string `yaml:"username"`
|
|
||||||
Displayname string `yaml:"displayname"`
|
|
||||||
Avatar string `yaml:"avatar"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bot) validate() error {
|
|
||||||
if b.Username == "" {
|
|
||||||
b.Username = "discordbot"
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Displayname == "" {
|
|
||||||
b.Displayname = "Discord Bridge Bot"
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bot) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawBot bot
|
|
||||||
|
|
||||||
raw := rawBot{}
|
|
||||||
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*b = bot(raw)
|
|
||||||
|
|
||||||
return b.validate()
|
|
||||||
}
|
|
153
config/bridge.go
153
config/bridge.go
|
@ -1,3 +1,19 @@
|
||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// Copyright (C) 2022 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 config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -5,19 +21,19 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bridge struct {
|
type BridgeConfig struct {
|
||||||
UsernameTemplate string `yaml:"username_template"`
|
UsernameTemplate string `yaml:"username_template"`
|
||||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||||
ChannelnameTemplate string `yaml:"channelname_template"`
|
ChannelnameTemplate string `yaml:"channelname_template"`
|
||||||
|
|
||||||
CommandPrefix string `yaml:"command_prefix"`
|
CommandPrefix string `yaml:"command_prefix"`
|
||||||
|
|
||||||
ManagementRoomText managementRoomText `yaml:"management_root_text"`
|
ManagementRoomText bridgeconfig.ManagementRoomTexts `yaml:"management_room_text"`
|
||||||
|
|
||||||
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
||||||
|
|
||||||
|
@ -30,127 +46,81 @@ type bridge struct {
|
||||||
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||||
LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
|
LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
|
||||||
|
|
||||||
Encryption encryption `yaml:"encryption"`
|
Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"`
|
||||||
|
|
||||||
|
Provisioning struct {
|
||||||
|
Prefix string `yaml:"prefix"`
|
||||||
|
SharedSecret string `yaml:"shared_secret"`
|
||||||
|
} `yaml:"provisioning"`
|
||||||
|
|
||||||
|
Permissions bridgeconfig.PermissionConfig `yaml:"permissions"`
|
||||||
|
|
||||||
usernameTemplate *template.Template `yaml:"-"`
|
usernameTemplate *template.Template `yaml:"-"`
|
||||||
displaynameTemplate *template.Template `yaml:"-"`
|
displaynameTemplate *template.Template `yaml:"-"`
|
||||||
channelnameTemplate *template.Template `yaml:"-"`
|
channelnameTemplate *template.Template `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
|
type umBridgeConfig BridgeConfig
|
||||||
_, homeserver, _ := userID.Parse()
|
|
||||||
_, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver]
|
|
||||||
|
|
||||||
return hasSecret
|
func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
}
|
err := unmarshal((*umBridgeConfig)(bc))
|
||||||
|
|
||||||
func (b *bridge) validate() error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if b.UsernameTemplate == "" {
|
|
||||||
b.UsernameTemplate = "discord_{{.}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
b.usernameTemplate, err = template.New("username").Parse(b.UsernameTemplate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.DisplaynameTemplate == "" {
|
bc.usernameTemplate, err = template.New("username").Parse(bc.UsernameTemplate)
|
||||||
b.DisplaynameTemplate = "{{.Username}}#{{.Discriminator}} (D){{if .Bot}} (bot){{end}}"
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !strings.Contains(bc.FormatUsername("1234567890"), "1234567890") {
|
||||||
|
return fmt.Errorf("username template is missing user ID placeholder")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.displaynameTemplate, err = template.New("displayname").Parse(b.DisplaynameTemplate)
|
bc.displaynameTemplate, err = template.New("displayname").Parse(bc.DisplaynameTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.ChannelnameTemplate == "" {
|
bc.channelnameTemplate, err = template.New("channelname").Parse(bc.ChannelnameTemplate)
|
||||||
b.ChannelnameTemplate = "{{if .Guild}}{{.Guild}} - {{end}}{{if .Folder}}{{.Folder}} - {{end}}{{.Name}} (D)"
|
|
||||||
}
|
|
||||||
|
|
||||||
b.channelnameTemplate, err = template.New("channelname").Parse(b.ChannelnameTemplate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.PortalMessageBuffer <= 0 {
|
|
||||||
b.PortalMessageBuffer = 128
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.CommandPrefix == "" {
|
|
||||||
b.CommandPrefix = "!dis"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.ManagementRoomText.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bridge) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
var _ bridgeconfig.BridgeConfig = (*BridgeConfig)(nil)
|
||||||
type rawBridge bridge
|
|
||||||
|
|
||||||
// Set our defaults that aren't zero values.
|
func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig {
|
||||||
raw := rawBridge{
|
return bc.Encryption
|
||||||
SyncWithCustomPuppets: true,
|
|
||||||
DefaultBridgeReceipts: true,
|
|
||||||
DefaultBridgePresence: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := unmarshal(&raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*b = bridge(raw)
|
|
||||||
|
|
||||||
return b.validate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b bridge) FormatUsername(userid string) string {
|
func (bc BridgeConfig) GetCommandPrefix() string {
|
||||||
|
return bc.CommandPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) GetManagementRoomTexts() bridgeconfig.ManagementRoomTexts {
|
||||||
|
return bc.ManagementRoomText
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) FormatUsername(userid string) string {
|
||||||
var buffer strings.Builder
|
var buffer strings.Builder
|
||||||
|
_ = bc.usernameTemplate.Execute(&buffer, userid)
|
||||||
b.usernameTemplate.Execute(&buffer, userid)
|
|
||||||
|
|
||||||
return buffer.String()
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type simplfiedUser struct {
|
func (bc BridgeConfig) FormatDisplayname(user *discordgo.User) string {
|
||||||
Username string
|
|
||||||
Discriminator string
|
|
||||||
Locale string
|
|
||||||
Verified bool
|
|
||||||
MFAEnabled bool
|
|
||||||
Bot bool
|
|
||||||
System bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b bridge) FormatDisplayname(user *discordgo.User) string {
|
|
||||||
var buffer strings.Builder
|
var buffer strings.Builder
|
||||||
|
_ = bc.displaynameTemplate.Execute(&buffer, user)
|
||||||
b.displaynameTemplate.Execute(&buffer, simplfiedUser{
|
|
||||||
Username: user.Username,
|
|
||||||
Discriminator: user.Discriminator,
|
|
||||||
Locale: user.Locale,
|
|
||||||
Verified: user.Verified,
|
|
||||||
MFAEnabled: user.MFAEnabled,
|
|
||||||
Bot: user.Bot,
|
|
||||||
System: user.System,
|
|
||||||
})
|
|
||||||
|
|
||||||
return buffer.String()
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type simplfiedChannel struct {
|
type wrappedChannel struct {
|
||||||
|
*discordgo.Channel
|
||||||
Guild string
|
Guild string
|
||||||
Folder string
|
Folder string
|
||||||
Name string
|
|
||||||
NSFW bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b bridge) FormatChannelname(channel *discordgo.Channel, session *discordgo.Session) (string, error) {
|
func (bc BridgeConfig) FormatChannelname(channel *discordgo.Channel, session *discordgo.Session) (string, error) {
|
||||||
var buffer strings.Builder
|
var buffer strings.Builder
|
||||||
var guildName, folderName string
|
var guildName, folderName string
|
||||||
|
|
||||||
|
@ -171,18 +141,17 @@ func (b bridge) FormatChannelname(channel *discordgo.Channel, session *discordgo
|
||||||
if channel.Name == "" {
|
if channel.Name == "" {
|
||||||
recipients := make([]string, len(channel.Recipients))
|
recipients := make([]string, len(channel.Recipients))
|
||||||
for idx, user := range channel.Recipients {
|
for idx, user := range channel.Recipients {
|
||||||
recipients[idx] = b.FormatDisplayname(user)
|
recipients[idx] = bc.FormatDisplayname(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(recipients, ", "), nil
|
return strings.Join(recipients, ", "), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.channelnameTemplate.Execute(&buffer, simplfiedChannel{
|
_ = bc.channelnameTemplate.Execute(&buffer, wrappedChannel{
|
||||||
Guild: guildName,
|
Channel: channel,
|
||||||
Folder: folderName,
|
Guild: guildName,
|
||||||
Name: channel.Name,
|
Folder: folderName,
|
||||||
NSFW: channel.NSFW,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return buffer.String(), nil
|
return buffer.String(), nil
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/globals"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cmd struct {
|
|
||||||
HomeserverAddress string `kong:"arg,help='The url to for the homeserver',required='1'"`
|
|
||||||
Domain string `kong:"arg,help='The domain for the homeserver',required='1'"`
|
|
||||||
|
|
||||||
Force bool `kong:"flag,help='Overwrite an existing configuration file if one already exists',short='f',default='0'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cmd) Run(g *globals.Globals) error {
|
|
||||||
if _, err := os.Stat(g.Config); err == nil {
|
|
||||||
if c.Force == false {
|
|
||||||
return fmt.Errorf("file %q exists, use -f to overwrite", g.Config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &Config{
|
|
||||||
Homeserver: homeserver{
|
|
||||||
Address: c.HomeserverAddress,
|
|
||||||
Domain: c.Domain,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg.Save(g.Config)
|
|
||||||
}
|
|
114
config/config.go
114
config/config.go
|
@ -1,101 +1,35 @@
|
||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// Copyright (C) 2022 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 config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
"io/ioutil"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Homeserver homeserver `yaml:"homeserver"`
|
*bridgeconfig.BaseConfig `yaml:",inline"`
|
||||||
Appservice appservice `yaml:"appservice"`
|
|
||||||
Bridge bridge `yaml:"bridge"`
|
|
||||||
Logging logging `yaml:"logging"`
|
|
||||||
|
|
||||||
filename string `yaml:"-"`
|
Bridge BridgeConfig `yaml:"bridge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var configUpdated bool
|
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
|
||||||
|
_, homeserver, _ := userID.Parse()
|
||||||
|
_, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver]
|
||||||
|
|
||||||
func (cfg *Config) validate() error {
|
return hasSecret
|
||||||
if err := cfg.Homeserver.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.Appservice.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.Bridge.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.Logging.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if configUpdated {
|
|
||||||
return cfg.Save(cfg.filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawConfig Config
|
|
||||||
|
|
||||||
raw := rawConfig{
|
|
||||||
filename: cfg.filename,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*cfg = Config(raw)
|
|
||||||
|
|
||||||
return cfg.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromBytes(filename string, data []byte) (*Config, error) {
|
|
||||||
cfg := Config{
|
|
||||||
filename: filename,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromString(str string) (*Config, error) {
|
|
||||||
return FromBytes("", []byte(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromFile(filename string) (*Config, error) {
|
|
||||||
data, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return FromBytes(filename, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Save(filename string) error {
|
|
||||||
if filename == "" {
|
|
||||||
return fmt.Errorf("no filename specified yep")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := yaml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ioutil.WriteFile(filename, data, 0600)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
|
|
||||||
db "go.mau.fi/mautrix-discord/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
type database struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
URI string `yaml:"uri"`
|
|
||||||
|
|
||||||
MaxOpenConns int `yaml:"max_open_conns"`
|
|
||||||
MaxIdleConns int `yaml:"max_idle_conns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) validate() error {
|
|
||||||
if d.Type == "" {
|
|
||||||
d.Type = "sqlite3"
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.URI == "" {
|
|
||||||
d.URI = "mautrix-discord.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.MaxOpenConns == 0 {
|
|
||||||
d.MaxOpenConns = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.MaxIdleConns == 0 {
|
|
||||||
d.MaxIdleConns = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawDatabase database
|
|
||||||
|
|
||||||
raw := rawDatabase{}
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*d = database(raw)
|
|
||||||
|
|
||||||
return d.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) CreateDatabase(baseLog log.Logger) (*db.Database, error) {
|
|
||||||
return db.New(
|
|
||||||
c.Appservice.Database.Type,
|
|
||||||
c.Appservice.Database.URI,
|
|
||||||
c.Appservice.Database.MaxOpenConns,
|
|
||||||
c.Appservice.Database.MaxIdleConns,
|
|
||||||
baseLog,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
type encryption struct {
|
|
||||||
Allow bool `yaml:"allow"`
|
|
||||||
Default bool `yaml:"default"`
|
|
||||||
|
|
||||||
KeySharing struct {
|
|
||||||
Allow bool `yaml:"allow"`
|
|
||||||
RequireCrossSigning bool `yaml:"require_cross_signing"`
|
|
||||||
RequireVerification bool `yaml:"require_verification"`
|
|
||||||
} `yaml:"key_sharing"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encryption) validate() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encryption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawEncryption encryption
|
|
||||||
|
|
||||||
raw := rawEncryption{}
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*e = encryption(raw)
|
|
||||||
|
|
||||||
return e.validate()
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrHomeserverNoAddress = errors.New("no homeserver address specified")
|
|
||||||
ErrHomeserverNoDomain = errors.New("no homeserver domain specified")
|
|
||||||
)
|
|
||||||
|
|
||||||
type homeserver struct {
|
|
||||||
Address string `yaml:"address"`
|
|
||||||
Domain string `yaml:"domain"`
|
|
||||||
Asmux bool `yaml:"asmux"`
|
|
||||||
StatusEndpoint string `yaml:"status_endpoint"`
|
|
||||||
AsyncMedia bool `yaml:"async_media"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *homeserver) validate() error {
|
|
||||||
if h.Address == "" {
|
|
||||||
return ErrHomeserverNoAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Domain == "" {
|
|
||||||
return ErrHomeserverNoDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *homeserver) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawHomeserver homeserver
|
|
||||||
|
|
||||||
raw := rawHomeserver{}
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*h = homeserver(raw)
|
|
||||||
|
|
||||||
return h.validate()
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"maunium.net/go/maulogger/v2"
|
|
||||||
as "maunium.net/go/mautrix/appservice"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logging as.LogConfig
|
|
||||||
|
|
||||||
func (l *logging) validate() error {
|
|
||||||
if l.Directory == "" {
|
|
||||||
l.Directory = "./logs"
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.FileNameFormat == "" {
|
|
||||||
l.FileNameFormat = "{{.Date}}-{{.Index}}.log"
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.FileDateFormat == "" {
|
|
||||||
l.FileDateFormat = "2006-01-02"
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.FileMode == 0 {
|
|
||||||
l.FileMode = 384
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.TimestampFormat == "" {
|
|
||||||
l.TimestampFormat = "Jan _2, 2006 15:04:05"
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.RawPrintLevel == "" {
|
|
||||||
l.RawPrintLevel = "debug"
|
|
||||||
} else {
|
|
||||||
switch strings.ToUpper(l.RawPrintLevel) {
|
|
||||||
case "TRACE":
|
|
||||||
l.PrintLevel = -10
|
|
||||||
case "DEBUG":
|
|
||||||
l.PrintLevel = maulogger.LevelDebug.Severity
|
|
||||||
case "INFO":
|
|
||||||
l.PrintLevel = maulogger.LevelInfo.Severity
|
|
||||||
case "WARN", "WARNING":
|
|
||||||
l.PrintLevel = maulogger.LevelWarn.Severity
|
|
||||||
case "ERR", "ERROR":
|
|
||||||
l.PrintLevel = maulogger.LevelError.Severity
|
|
||||||
case "FATAL":
|
|
||||||
l.PrintLevel = maulogger.LevelFatal.Severity
|
|
||||||
default:
|
|
||||||
return errors.New("invalid print level " + l.RawPrintLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logging) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawLogging logging
|
|
||||||
|
|
||||||
raw := rawLogging{}
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*l = logging(raw)
|
|
||||||
|
|
||||||
return l.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) CreateLogger() (maulogger.Logger, error) {
|
|
||||||
logger := maulogger.Create()
|
|
||||||
|
|
||||||
// create an as.LogConfig from our config so we can configure the logger
|
|
||||||
realLogConfig := as.LogConfig(cfg.Logging)
|
|
||||||
realLogConfig.Configure(logger)
|
|
||||||
|
|
||||||
// Set the default logger.
|
|
||||||
maulogger.DefaultLogger = logger.(*maulogger.BasicLogger)
|
|
||||||
|
|
||||||
// If we were given a filename format attempt to open the file.
|
|
||||||
if cfg.Logging.FileNameFormat != "" {
|
|
||||||
if err := maulogger.OpenFile(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return logger, nil
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
type managementRoomText struct {
|
|
||||||
Welcome string `yaml:"welcome"`
|
|
||||||
Connected string `yaml:"welcome_connected"`
|
|
||||||
NotConnected string `yaml:"welcome_unconnected"`
|
|
||||||
AdditionalHelp string `yaml:"additional_help"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *managementRoomText) validate() error {
|
|
||||||
if m.Welcome == "" {
|
|
||||||
m.Welcome = "Greetings, I am a Discord bridge bot!"
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Connected == "" {
|
|
||||||
m.Connected = "Use `help` to get started."
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.NotConnected == "" {
|
|
||||||
m.NotConnected = "Use `help` to get started, or `login` to login."
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *managementRoomText) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawManagementRoomText managementRoomText
|
|
||||||
|
|
||||||
raw := rawManagementRoomText{}
|
|
||||||
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*m = managementRoomText(raw)
|
|
||||||
|
|
||||||
return m.validate()
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
as "maunium.net/go/mautrix/appservice"
|
|
||||||
)
|
|
||||||
|
|
||||||
type provisioning struct {
|
|
||||||
Prefix string `yaml:"prefix"`
|
|
||||||
SharedSecret string `yaml:"shared_secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioning) validate() error {
|
|
||||||
if p.Prefix == "" {
|
|
||||||
p.Prefix = "/_matrix/provision/v1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToLower(p.SharedSecret) == "generate" {
|
|
||||||
p.SharedSecret = as.RandomString(64)
|
|
||||||
|
|
||||||
configUpdated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioning) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type rawProvisioning provisioning
|
|
||||||
|
|
||||||
raw := rawProvisioning{}
|
|
||||||
if err := unmarshal(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*p = provisioning(raw)
|
|
||||||
|
|
||||||
return p.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioning) Enabled() bool {
|
|
||||||
return strings.ToLower(p.SharedSecret) != "disable"
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
as "maunium.net/go/mautrix/appservice"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Config) CopyToRegistration(registration *as.Registration) error {
|
|
||||||
registration.ID = cfg.Appservice.ID
|
|
||||||
registration.URL = cfg.Appservice.Address
|
|
||||||
registration.EphemeralEvents = cfg.Appservice.EphemeralEvents
|
|
||||||
|
|
||||||
falseVal := false
|
|
||||||
registration.RateLimited = &falseVal
|
|
||||||
|
|
||||||
registration.SenderLocalpart = cfg.Appservice.Bot.Username
|
|
||||||
|
|
||||||
pattern := fmt.Sprintf(
|
|
||||||
"^@%s:%s$",
|
|
||||||
cfg.Bridge.FormatUsername("[0-9]+"),
|
|
||||||
cfg.Homeserver.Domain,
|
|
||||||
)
|
|
||||||
|
|
||||||
userIDRegex, err := regexp.Compile(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
registration.Namespaces.RegisterUserIDs(userIDRegex, true)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) getRegistration() (*as.Registration, error) {
|
|
||||||
registration := as.CreateRegistration()
|
|
||||||
|
|
||||||
if err := cfg.CopyToRegistration(registration); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
registration.AppToken = cfg.Appservice.ASToken
|
|
||||||
registration.ServerToken = cfg.Appservice.HSToken
|
|
||||||
|
|
||||||
return registration, nil
|
|
||||||
}
|
|
79
config/upgrade.go
Normal file
79
config/upgrade.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// Copyright (C) 2022 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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
up "maunium.net/go/mautrix/util/configupgrade"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoUpgrade(helper *up.Helper) {
|
||||||
|
bridgeconfig.Upgrader.DoUpgrade(helper)
|
||||||
|
|
||||||
|
helper.Copy(up.Str, "bridge", "username_template")
|
||||||
|
helper.Copy(up.Str, "bridge", "displayname_template")
|
||||||
|
helper.Copy(up.Str, "bridge", "channelname_template")
|
||||||
|
helper.Copy(up.Int, "bridge", "portal_message_buffer")
|
||||||
|
helper.Copy(up.Bool, "bridge", "sync_with_custom_puppets")
|
||||||
|
helper.Copy(up.Bool, "bridge", "sync_direct_chat_list")
|
||||||
|
helper.Copy(up.Bool, "bridge", "default_bridge_receipts")
|
||||||
|
helper.Copy(up.Bool, "bridge", "default_bridge_presence")
|
||||||
|
helper.Copy(up.Map, "bridge", "double_puppet_server_map")
|
||||||
|
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
||||||
|
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
||||||
|
helper.Copy(up.Str, "bridge", "command_prefix")
|
||||||
|
helper.Copy(up.Str, "bridge", "management_room_text", "welcome")
|
||||||
|
helper.Copy(up.Str, "bridge", "management_room_text", "welcome_connected")
|
||||||
|
helper.Copy(up.Str, "bridge", "management_room_text", "welcome_unconnected")
|
||||||
|
helper.Copy(up.Str|up.Null, "bridge", "management_room_text", "additional_help")
|
||||||
|
helper.Copy(up.Bool, "bridge", "encryption", "allow")
|
||||||
|
helper.Copy(up.Bool, "bridge", "encryption", "default")
|
||||||
|
helper.Copy(up.Bool, "bridge", "encryption", "key_sharing", "allow")
|
||||||
|
helper.Copy(up.Bool, "bridge", "encryption", "key_sharing", "require_cross_signing")
|
||||||
|
helper.Copy(up.Bool, "bridge", "encryption", "key_sharing", "require_verification")
|
||||||
|
|
||||||
|
helper.Copy(up.Str, "bridge", "provisioning", "prefix")
|
||||||
|
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
|
||||||
|
sharedSecret := appservice.RandomString(64)
|
||||||
|
helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret")
|
||||||
|
} else {
|
||||||
|
helper.Copy(up.Str, "bridge", "provisioning", "shared_secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Copy(up.Map, "bridge", "permissions")
|
||||||
|
//helper.Copy(up.Bool, "bridge", "relay", "enabled")
|
||||||
|
//helper.Copy(up.Bool, "bridge", "relay", "admin_only")
|
||||||
|
//helper.Copy(up.Map, "bridge", "relay", "message_formats")
|
||||||
|
}
|
||||||
|
|
||||||
|
var SpacedBlocks = [][]string{
|
||||||
|
{"homeserver", "asmux"},
|
||||||
|
{"appservice"},
|
||||||
|
{"appservice", "hostname"},
|
||||||
|
{"appservice", "database"},
|
||||||
|
{"appservice", "id"},
|
||||||
|
{"appservice", "as_token"},
|
||||||
|
{"bridge"},
|
||||||
|
{"bridge", "command_prefix"},
|
||||||
|
{"bridge", "management_room_text"},
|
||||||
|
{"bridge", "encryption"},
|
||||||
|
{"bridge", "provisioning"},
|
||||||
|
{"bridge", "permissions"},
|
||||||
|
//{"bridge", "relay"},
|
||||||
|
{"logging"},
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
package consts
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "mautrix-discord"
|
|
||||||
Description = "Discord-Matrix puppeting bridge"
|
|
||||||
)
|
|
337
custompuppet.go
Normal file
337
custompuppet.go
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoCustomMXID = errors.New("no custom mxid set")
|
||||||
|
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// additional bridge api
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
func (br *DiscordBridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
|
||||||
|
_, homeserver, err := mxid.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
|
||||||
|
if !found {
|
||||||
|
if homeserver == br.AS.HomeserverDomain {
|
||||||
|
homeserverURL = br.AS.HomeserverURL
|
||||||
|
} else if br.Config.Bridge.DoublePuppetAllowDiscovery {
|
||||||
|
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeserverURL = resp.Homeserver.BaseURL
|
||||||
|
br.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := mautrix.NewClient(homeserverURL, mxid, accessToken)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// mautrix.Syncer implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
||||||
|
everything := []event.Type{{Type: "*"}}
|
||||||
|
return &mautrix.Filter{
|
||||||
|
Presence: mautrix.FilterPart{
|
||||||
|
Senders: []id.UserID{puppet.CustomMXID},
|
||||||
|
Types: []event.Type{event.EphemeralEventPresence},
|
||||||
|
},
|
||||||
|
AccountData: mautrix.FilterPart{NotTypes: everything},
|
||||||
|
Room: mautrix.RoomFilter{
|
||||||
|
Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
|
||||||
|
IncludeLeave: false,
|
||||||
|
AccountData: mautrix.FilterPart{NotTypes: everything},
|
||||||
|
State: mautrix.FilterPart{NotTypes: everything},
|
||||||
|
Timeline: mautrix.FilterPart{NotTypes: everything},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
||||||
|
puppet.log.Warnln("Sync error:", err)
|
||||||
|
if errors.Is(err, mautrix.MUnknownToken) {
|
||||||
|
if !puppet.tryRelogin(err, "syncing") {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.customIntent.AccessToken = puppet.AccessToken
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 10 * time.Second, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
|
||||||
|
if !puppet.customUser.IsLoggedIn() {
|
||||||
|
puppet.log.Debugln("Skipping sync processing: custom user not connected to discord")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for roomID, events := range resp.Rooms.Join {
|
||||||
|
// for _, evt := range events.Ephemeral.Events {
|
||||||
|
// evt.RoomID = roomID
|
||||||
|
// err := evt.Content.ParseRaw(evt.Type)
|
||||||
|
// if err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// switch evt.Type {
|
||||||
|
// case event.EphemeralEventReceipt:
|
||||||
|
// if puppet.EnableReceipts {
|
||||||
|
// go puppet.bridge.MatrixHandler.HandleReceipt(evt)
|
||||||
|
// }
|
||||||
|
// case event.EphemeralEventTyping:
|
||||||
|
// go puppet.bridge.MatrixHandler.HandleTyping(evt)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if puppet.EnablePresence {
|
||||||
|
// for _, evt := range resp.Presence.Events {
|
||||||
|
// if evt.Sender != puppet.CustomMXID {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// err := evt.Content.ParseRaw(evt.Type)
|
||||||
|
// if err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// go puppet.bridge.MatrixHandler.HandlePresence(evt)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// mautrix.Storer implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) {
|
||||||
|
puppet.NextBatch = nbt
|
||||||
|
puppet.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) LoadFilterID(_ id.UserID) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) LoadNextBatch(_ id.UserID) string {
|
||||||
|
return puppet.NextBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// additional puppet api
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
func (puppet *Puppet) clearCustomMXID() {
|
||||||
|
puppet.CustomMXID = ""
|
||||||
|
puppet.AccessToken = ""
|
||||||
|
puppet.customIntent = nil
|
||||||
|
puppet.customUser = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
|
||||||
|
if puppet.CustomMXID == "" {
|
||||||
|
return nil, ErrNoCustomMXID
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := puppet.bridge.newDoublePuppetClient(puppet.CustomMXID, puppet.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Syncer = puppet
|
||||||
|
client.Store = puppet
|
||||||
|
|
||||||
|
ia := puppet.bridge.AS.NewIntentAPI("custom")
|
||||||
|
ia.Client = client
|
||||||
|
ia.Localpart, _, _ = puppet.CustomMXID.Parse()
|
||||||
|
ia.UserID = puppet.CustomMXID
|
||||||
|
ia.IsCustomPuppet = true
|
||||||
|
|
||||||
|
return ia, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
|
||||||
|
if puppet.CustomMXID == "" {
|
||||||
|
puppet.clearCustomMXID()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
intent, err := puppet.newCustomIntent()
|
||||||
|
if err != nil {
|
||||||
|
puppet.clearCustomMXID()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := intent.Whoami()
|
||||||
|
if err != nil {
|
||||||
|
if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !puppet.tryRelogin(err, "initializing double puppeting")) {
|
||||||
|
puppet.clearCustomMXID()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.AccessToken = puppet.AccessToken
|
||||||
|
} else if resp.UserID != puppet.CustomMXID {
|
||||||
|
puppet.clearCustomMXID()
|
||||||
|
|
||||||
|
return ErrMismatchingMXID
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.customIntent = intent
|
||||||
|
puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
|
||||||
|
puppet.startSyncing()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) tryRelogin(cause error, action string) bool {
|
||||||
|
if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action)
|
||||||
|
|
||||||
|
accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action)
|
||||||
|
puppet.AccessToken = accessToken
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) startSyncing() {
|
||||||
|
if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
puppet.log.Debugln("Starting syncing...")
|
||||||
|
puppet.customIntent.SyncPresence = "offline"
|
||||||
|
|
||||||
|
err := puppet.customIntent.Sync()
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Errorln("Fatal error syncing:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) stopSyncing() {
|
||||||
|
if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.customIntent.StopSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
|
||||||
|
_, homeserver, _ := mxid.Parse()
|
||||||
|
|
||||||
|
puppet.log.Debugfln("Logging into %s with shared secret", mxid)
|
||||||
|
|
||||||
|
mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]))
|
||||||
|
mac.Write([]byte(mxid))
|
||||||
|
|
||||||
|
client, err := puppet.bridge.newDoublePuppetClient(mxid, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Login(&mautrix.ReqLogin{
|
||||||
|
Type: mautrix.AuthTypePassword,
|
||||||
|
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
|
||||||
|
Password: hex.EncodeToString(mac.Sum(nil)),
|
||||||
|
DeviceID: "Discord Bridge",
|
||||||
|
InitialDeviceDisplayName: "Discord Bridge",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
|
||||||
|
prevCustomMXID := puppet.CustomMXID
|
||||||
|
if puppet.customIntent != nil {
|
||||||
|
puppet.stopSyncing()
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.CustomMXID = mxid
|
||||||
|
puppet.AccessToken = accessToken
|
||||||
|
|
||||||
|
err := puppet.StartCustomMXID(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if prevCustomMXID != "" {
|
||||||
|
delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if puppet.CustomMXID != "" {
|
||||||
|
puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence
|
||||||
|
puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts
|
||||||
|
|
||||||
|
puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID)
|
||||||
|
|
||||||
|
puppet.Update()
|
||||||
|
|
||||||
|
// TODO leave rooms with default puppet
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
|
@ -19,7 +21,7 @@ type Attachment struct {
|
||||||
MatrixEventID id.EventID
|
MatrixEventID id.EventID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Attachment) Scan(row Scannable) *Attachment {
|
func (a *Attachment) Scan(row dbutil.Scannable) *Attachment {
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&a.Channel.ChannelID, &a.Channel.Receiver,
|
&a.Channel.ChannelID, &a.Channel.Receiver,
|
||||||
&a.DiscordMessageID, &a.DiscordAttachmentID,
|
&a.DiscordMessageID, &a.DiscordAttachmentID,
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix/crypto"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SQLCryptoStore struct {
|
|
||||||
*crypto.SQLCryptoStore
|
|
||||||
UserID id.UserID
|
|
||||||
GhostIDFormat string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ crypto.Store = (*SQLCryptoStore)(nil)
|
|
||||||
|
|
||||||
func NewSQLCryptoStore(db *Database, userID id.UserID, ghostIDFormat string) *SQLCryptoStore {
|
|
||||||
return &SQLCryptoStore{
|
|
||||||
SQLCryptoStore: crypto.NewSQLCryptoStore(db.DB, db.dialect, "", "",
|
|
||||||
[]byte("maunium.net/go/mautrix-whatsapp"),
|
|
||||||
&cryptoLogger{db.log.Sub("CryptoStore")}),
|
|
||||||
UserID: userID,
|
|
||||||
GhostIDFormat: ghostIDFormat,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *SQLCryptoStore) FindDeviceID() id.DeviceID {
|
|
||||||
var deviceID id.DeviceID
|
|
||||||
|
|
||||||
query := `SELECT device_id FROM crypto_account WHERE account_id=$1`
|
|
||||||
err := store.DB.QueryRow(query, store.AccountID).Scan(&deviceID)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
store.Log.Warn("Failed to scan device ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) ([]id.UserID, error) {
|
|
||||||
query := `
|
|
||||||
SELECT user_id FROM mx_user_profile
|
|
||||||
WHERE room_id=$1
|
|
||||||
AND (membership='join' OR membership='invite')
|
|
||||||
AND user_id<>$2
|
|
||||||
AND user_id NOT LIKE $3
|
|
||||||
`
|
|
||||||
|
|
||||||
members := []id.UserID{}
|
|
||||||
|
|
||||||
rows, err := store.DB.Query(query, roomID, store.UserID, store.GhostIDFormat)
|
|
||||||
if err != nil {
|
|
||||||
return members, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var userID id.UserID
|
|
||||||
err := rows.Scan(&userID)
|
|
||||||
if err != nil {
|
|
||||||
store.Log.Warn("Failed to scan member in %s: %v", roomID, err)
|
|
||||||
return members, err
|
|
||||||
}
|
|
||||||
|
|
||||||
members = append(members, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return members, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO merge this with the one in the parent package
|
|
||||||
type cryptoLogger struct {
|
|
||||||
int log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
var levelTrace = log.Level{
|
|
||||||
Name: "TRACE",
|
|
||||||
Severity: -10,
|
|
||||||
Color: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Error(message string, args ...interface{}) {
|
|
||||||
c.int.Errorfln(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Warn(message string, args ...interface{}) {
|
|
||||||
c.int.Warnfln(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Debug(message string, args ...interface{}) {
|
|
||||||
c.int.Debugfln(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptoLogger) Trace(message string, args ...interface{}) {
|
|
||||||
c.int.Logfln(levelTrace, message, args...)
|
|
||||||
}
|
|
|
@ -1,20 +1,18 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
"go.mau.fi/mautrix-discord/database/upgrades"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
"go.mau.fi/mautrix-discord/database/migrations"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Database struct {
|
type Database struct {
|
||||||
*sql.DB
|
*dbutil.Database
|
||||||
log log.Logger
|
|
||||||
dialect string
|
|
||||||
|
|
||||||
User *UserQuery
|
User *UserQuery
|
||||||
Portal *PortalQuery
|
Portal *PortalQuery
|
||||||
|
@ -26,70 +24,51 @@ type Database struct {
|
||||||
Guild *GuildQuery
|
Guild *GuildQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dbType, uri string, maxOpenConns, maxIdleConns int, baseLog log.Logger) (*Database, error) {
|
//go:embed legacymigrate.sql
|
||||||
conn, err := sql.Open(dbType, uri)
|
var legacyMigrate string
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
func New(baseDB *dbutil.Database) *Database {
|
||||||
|
db := &Database{Database: baseDB}
|
||||||
|
_, err := db.Exec("SELECT id FROM version")
|
||||||
|
if err == nil {
|
||||||
|
baseDB.Log.Infoln("Migrating from legacy database versioning")
|
||||||
|
_, err = db.Exec(legacyMigrate)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to migrate from legacy database versioning: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
db.UpgradeTable = upgrades.Table
|
||||||
if dbType == "sqlite3" {
|
|
||||||
conn.Exec("PRAGMA foreign_keys = ON")
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.SetMaxOpenConns(maxOpenConns)
|
|
||||||
conn.SetMaxIdleConns(maxIdleConns)
|
|
||||||
|
|
||||||
dbLog := baseLog.Sub("Database")
|
|
||||||
|
|
||||||
if err := migrations.Run(conn, dbLog, dbType); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
db := &Database{
|
|
||||||
DB: conn,
|
|
||||||
log: dbLog,
|
|
||||||
dialect: dbType,
|
|
||||||
}
|
|
||||||
|
|
||||||
db.User = &UserQuery{
|
db.User = &UserQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("User"),
|
log: db.Log.Sub("User"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Portal = &PortalQuery{
|
db.Portal = &PortalQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Portal"),
|
log: db.Log.Sub("Portal"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Puppet = &PuppetQuery{
|
db.Puppet = &PuppetQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Puppet"),
|
log: db.Log.Sub("Puppet"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Message = &MessageQuery{
|
db.Message = &MessageQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Message"),
|
log: db.Log.Sub("Message"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Reaction = &ReactionQuery{
|
db.Reaction = &ReactionQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Reaction"),
|
log: db.Log.Sub("Reaction"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Attachment = &AttachmentQuery{
|
db.Attachment = &AttachmentQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Attachment"),
|
log: db.Log.Sub("Attachment"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Emoji = &EmojiQuery{
|
db.Emoji = &EmojiQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Emoji"),
|
log: db.Log.Sub("Emoji"),
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Guild = &GuildQuery{
|
db.Guild = &GuildQuery{
|
||||||
db: db,
|
db: db,
|
||||||
log: db.log.Sub("Guild"),
|
log: db.Log.Sub("Guild"),
|
||||||
}
|
}
|
||||||
|
return db
|
||||||
return db, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Emoji struct {
|
type Emoji struct {
|
||||||
|
@ -19,7 +20,7 @@ type Emoji struct {
|
||||||
MatrixURL id.ContentURI
|
MatrixURL id.ContentURI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Emoji) Scan(row Scannable) *Emoji {
|
func (e *Emoji) Scan(row dbutil.Scannable) *Emoji {
|
||||||
var matrixURL sql.NullString
|
var matrixURL sql.NullString
|
||||||
err := row.Scan(&e.DiscordID, &e.DiscordName, &matrixURL)
|
err := row.Scan(&e.DiscordID, &e.DiscordName, &matrixURL)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Guild struct {
|
type Guild struct {
|
||||||
|
@ -17,7 +19,7 @@ type Guild struct {
|
||||||
Bridge bool
|
Bridge bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Guild) Scan(row Scannable) *Guild {
|
func (g *Guild) Scan(row dbutil.Scannable) *Guild {
|
||||||
err := row.Scan(&g.DiscordID, &g.GuildID, &g.GuildName, &g.Bridge)
|
err := row.Scan(&g.DiscordID, &g.GuildID, &g.GuildName, &g.Bridge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, sql.ErrNoRows) {
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
|
10
database/legacymigrate.sql
Normal file
10
database/legacymigrate.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
DROP TABLE version;
|
||||||
|
CREATE TABLE version(version INTEGER PRIMARY KEY);
|
||||||
|
INSERT INTO version VALUES (1);
|
||||||
|
CREATE TABLE crypto_version (version INTEGER PRIMARY KEY);
|
||||||
|
INSERT INTO crypto_version VALUES (6);
|
||||||
|
CREATE TABLE mx_version (version INTEGER PRIMARY KEY);
|
||||||
|
INSERT INTO mx_version VALUES (1);
|
||||||
|
|
||||||
|
UPDATE "user" SET id=null WHERE id='';
|
||||||
|
ALTER TABLE "user" ADD CONSTRAINT user_id_key UNIQUE (id);
|
|
@ -6,7 +6,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
@ -22,7 +24,7 @@ type Message struct {
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) Scan(row Scannable) *Message {
|
func (m *Message) Scan(row dbutil.Scannable) *Message {
|
||||||
var ts int64
|
var ts int64
|
||||||
|
|
||||||
err := row.Scan(&m.Channel.ChannelID, &m.Channel.Receiver, &m.DiscordID, &m.MatrixID, &m.AuthorID, &ts)
|
err := row.Scan(&m.Channel.ChannelID, &m.Channel.Receiver, &m.DiscordID, &m.MatrixID, &m.AuthorID, &ts)
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
CREATE TABLE attachment (
|
|
||||||
channel_id TEXT NOT NULL,
|
|
||||||
receiver TEXT NOT NULL,
|
|
||||||
|
|
||||||
discord_message_id TEXT NOT NULL,
|
|
||||||
discord_attachment_id TEXT NOT NULL,
|
|
||||||
|
|
||||||
matrix_event_id TEXT NOT NULL UNIQUE,
|
|
||||||
|
|
||||||
PRIMARY KEY(discord_attachment_id, matrix_event_id),
|
|
||||||
FOREIGN KEY(channel_id, receiver) REFERENCES portal(channel_id, receiver) ON DELETE CASCADE
|
|
||||||
);
|
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE TABLE emoji (
|
|
||||||
discord_id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
discord_name TEXT,
|
|
||||||
matrix_url TEXT
|
|
||||||
);
|
|
|
@ -1,2 +0,0 @@
|
||||||
ALTER TABLE puppet ADD COLUMN custom_mxid TEXT;
|
|
||||||
ALTER TABLE puppet ADD COLUMN access_token TEXT;
|
|
|
@ -1,2 +0,0 @@
|
||||||
ALTER TABLE puppet ADD COLUMN next_batch TEXT;
|
|
||||||
ALTER TABLE puppet ADD COLUMN enable_receipts BOOLEAN NOT NULL DEFAULT true;
|
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE "user" DROP CONSTRAINT user_id_key;
|
|
|
@ -1,18 +0,0 @@
|
||||||
PRAGMA foreign_keys=off;
|
|
||||||
|
|
||||||
ALTER TABLE "user" RENAME TO "old_user";
|
|
||||||
|
|
||||||
CREATE TABLE "user" (
|
|
||||||
mxid TEXT PRIMARY KEY,
|
|
||||||
id TEXT,
|
|
||||||
|
|
||||||
management_room TEXT,
|
|
||||||
|
|
||||||
token TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO "user" SELECT mxid, id, management_room, token FROM "old_user";
|
|
||||||
|
|
||||||
DROP TABLE "old_user";
|
|
||||||
|
|
||||||
PRAGMA foreign_keys=on;
|
|
|
@ -1,7 +0,0 @@
|
||||||
CREATE TABLE guild (
|
|
||||||
discord_id TEXT NOT NULL,
|
|
||||||
guild_id TEXT NOT NULL,
|
|
||||||
guild_name TEXT NOT NULL,
|
|
||||||
bridge BOOLEAN DEFAULT FALSE,
|
|
||||||
PRIMARY KEY(discord_id, guild_id)
|
|
||||||
);
|
|
|
@ -1,3 +0,0 @@
|
||||||
-- This migration is implemented in migrations.go as it comes from
|
|
||||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 0
|
|
||||||
-- which is described as "Add crypto store to database".
|
|
|
@ -1,3 +0,0 @@
|
||||||
-- This migration is implemented in migrations.go as it comes from
|
|
||||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 1
|
|
||||||
-- which is described as "Add account_id to crypto store".
|
|
|
@ -1,3 +0,0 @@
|
||||||
-- This migration is implemented in migrations.go as it comes from
|
|
||||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 2
|
|
||||||
-- which is described as "Add megolm withheld data to crypto store".
|
|
|
@ -1,3 +0,0 @@
|
||||||
-- This migration is implemented in migrations.go as it comes from
|
|
||||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 3
|
|
||||||
-- which is described as "Add cross-signing keys to crypto store".
|
|
|
@ -1,4 +0,0 @@
|
||||||
-- This migration is implemented in migrations.go as it comes from
|
|
||||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 4
|
|
||||||
-- which is described as "Replace VARCHAR(255) with TEXT in the crypto
|
|
||||||
-- database".
|
|
|
@ -1,4 +0,0 @@
|
||||||
-- This migration is implemented in migrations.go as it comes from
|
|
||||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 5
|
|
||||||
-- which is described as "Split last_used into last_encrypted and
|
|
||||||
-- last_decrypted in crypto store".
|
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE portal ADD COLUMN encrypted BOOLEAN NOT NULL DEFAULT false;
|
|
|
@ -1,120 +0,0 @@
|
||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"embed"
|
|
||||||
|
|
||||||
"github.com/lopezator/migrator"
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
"maunium.net/go/mautrix/crypto/sql_store_upgrade"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed *.sql
|
|
||||||
var embeddedMigrations embed.FS
|
|
||||||
|
|
||||||
func migrationFromFile(description, filename string) *migrator.Migration {
|
|
||||||
return &migrator.Migration{
|
|
||||||
Name: description,
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
data, err := embeddedMigrations.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tx.Exec(string(data)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrationFromFileWithDialect(dialect, description, sqliteFile, postgresFile string) *migrator.Migration {
|
|
||||||
switch dialect {
|
|
||||||
case "sqlite3":
|
|
||||||
return migrationFromFile(description, sqliteFile)
|
|
||||||
case "postgres":
|
|
||||||
return migrationFromFile(description, postgresFile)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(db *sql.DB, baseLog log.Logger, dialect string) error {
|
|
||||||
subLogger := baseLog.Sub("Migrations")
|
|
||||||
logger := migrator.LoggerFunc(func(msg string, args ...interface{}) {
|
|
||||||
subLogger.Infof(msg, args...)
|
|
||||||
})
|
|
||||||
|
|
||||||
m, err := migrator.New(
|
|
||||||
migrator.TableName("version"),
|
|
||||||
migrator.WithLogger(logger),
|
|
||||||
migrator.Migrations(
|
|
||||||
migrationFromFile("Initial Schema", "01-initial.sql"),
|
|
||||||
migrationFromFile("Attachments", "02-attachments.sql"),
|
|
||||||
migrationFromFile("Emoji", "03-emoji.sql"),
|
|
||||||
migrationFromFile("Custom Puppets", "04-custom-puppet.sql"),
|
|
||||||
migrationFromFile(
|
|
||||||
"Additional puppet fields",
|
|
||||||
"05-additional-puppet-fields.sql",
|
|
||||||
),
|
|
||||||
migrationFromFileWithDialect(
|
|
||||||
dialect,
|
|
||||||
"Remove unique user constraint",
|
|
||||||
"06-remove-unique-user-constraint.sqlite.sql",
|
|
||||||
"06-remove-unique-user-constraint.postgres.sql",
|
|
||||||
),
|
|
||||||
migrationFromFile("Guild Bridging", "07-guilds.sql"),
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "Add crypto store to database",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
return sql_store_upgrade.Upgrades[0](tx, dialect)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "Add account_id to crypto store",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
return sql_store_upgrade.Upgrades[1](tx, dialect)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "Add megolm withheld data to crypto store",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
return sql_store_upgrade.Upgrades[2](tx, dialect)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "Add cross-signing keys to crypto store",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
return sql_store_upgrade.Upgrades[3](tx, dialect)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "Replace VARCHAR(255) with TEXT in the crypto database",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
return sql_store_upgrade.Upgrades[4](tx, dialect)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "Split last_used into last_encrypted and last_decrypted in crypto store",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
return sql_store_upgrade.Upgrades[5](tx, dialect)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
migrationFromFile(
|
|
||||||
"Add encryption column to portal table",
|
|
||||||
"14-add-encrypted-column-to-portal-table.sql",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Migrate(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -6,7 +6,9 @@ import (
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Portal struct {
|
type Portal struct {
|
||||||
|
@ -30,7 +32,7 @@ type Portal struct {
|
||||||
FirstEventID id.EventID
|
FirstEventID id.EventID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Portal) Scan(row Scannable) *Portal {
|
func (p *Portal) Scan(row dbutil.Scannable) *Portal {
|
||||||
var mxid, avatarURL, firstEventID sql.NullString
|
var mxid, avatarURL, firstEventID sql.NullString
|
||||||
var typ sql.NullInt32
|
var typ sql.NullInt32
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -34,7 +36,7 @@ type Puppet struct {
|
||||||
EnableReceipts bool
|
EnableReceipts bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Puppet) Scan(row Scannable) *Puppet {
|
func (p *Puppet) Scan(row dbutil.Scannable) *Puppet {
|
||||||
var did, displayName, avatar, avatarURL sql.NullString
|
var did, displayName, avatar, avatarURL sql.NullString
|
||||||
var enablePresence sql.NullBool
|
var enablePresence sql.NullBool
|
||||||
var customMXID, accessToken, nextBatch sql.NullString
|
var customMXID, accessToken, nextBatch sql.NullString
|
||||||
|
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reaction struct {
|
type Reaction struct {
|
||||||
|
@ -26,7 +28,7 @@ type Reaction struct {
|
||||||
DiscordID string // The id or unicode of the emoji for discord
|
DiscordID string // The id or unicode of the emoji for discord
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reaction) Scan(row Scannable) *Reaction {
|
func (r *Reaction) Scan(row dbutil.Scannable) *Reaction {
|
||||||
var discordID sql.NullString
|
var discordID sql.NullString
|
||||||
|
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
type Scannable interface {
|
|
||||||
Scan(...interface{}) error
|
|
||||||
}
|
|
|
@ -1,304 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SQLStateStore struct {
|
|
||||||
*appservice.TypingStateStore
|
|
||||||
|
|
||||||
db *Database
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
Typing map[id.RoomID]map[id.UserID]int64
|
|
||||||
typingLock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure that SQLStateStore implements the appservice.StateStore interface
|
|
||||||
var _ appservice.StateStore = (*SQLStateStore)(nil)
|
|
||||||
|
|
||||||
func NewSQLStateStore(db *Database) *SQLStateStore {
|
|
||||||
return &SQLStateStore{
|
|
||||||
TypingStateStore: appservice.NewTypingStateStore(),
|
|
||||||
db: db,
|
|
||||||
log: db.log.Sub("StateStore"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) IsRegistered(userID id.UserID) bool {
|
|
||||||
var isRegistered bool
|
|
||||||
|
|
||||||
query := "SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)"
|
|
||||||
row := s.db.QueryRow(query, userID)
|
|
||||||
|
|
||||||
err := row.Scan(&isRegistered)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warnfln("Failed to scan registration existence for %s: %v", userID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return isRegistered
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) MarkRegistered(userID id.UserID) {
|
|
||||||
query := "INSERT INTO mx_registrations (user_id) VALUES ($1)" +
|
|
||||||
" ON CONFLICT (user_id) DO NOTHING"
|
|
||||||
|
|
||||||
_, err := s.db.Exec(query, userID)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warnfln("Failed to mark %s as registered: %v", userID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) IsTyping(roomID id.RoomID, userID id.UserID) bool {
|
|
||||||
s.log.Debugln("IsTyping")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) SetTyping(roomID id.RoomID, userID id.UserID, timeout int64) {
|
|
||||||
s.log.Debugln("SetTyping")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool {
|
|
||||||
return s.IsMembership(roomID, userID, "join")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool {
|
|
||||||
return s.IsMembership(roomID, userID, "join", "invite")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool {
|
|
||||||
membership := s.GetMembership(roomID, userID)
|
|
||||||
for _, allowedMembership := range allowedMemberships {
|
|
||||||
if allowedMembership == membership {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership {
|
|
||||||
query := "SELECT membership FROM mx_user_profile WHERE " +
|
|
||||||
"room_id=$1 AND user_id=$2"
|
|
||||||
row := s.db.QueryRow(query, roomID, userID)
|
|
||||||
|
|
||||||
membership := event.MembershipLeave
|
|
||||||
err := row.Scan(&membership)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
s.log.Warnfln("Failed to scan membership of %s in %s: %v", userID, roomID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return membership
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent {
|
|
||||||
member, ok := s.TryGetMember(roomID, userID)
|
|
||||||
if !ok {
|
|
||||||
member.Membership = event.MembershipLeave
|
|
||||||
}
|
|
||||||
|
|
||||||
return member
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) {
|
|
||||||
query := "SELECT membership, displayname, avatar_url FROM mx_user_profile " +
|
|
||||||
"WHERE room_id=$1 AND user_id=$2"
|
|
||||||
row := s.db.QueryRow(query, roomID, userID)
|
|
||||||
|
|
||||||
var member event.MemberEventContent
|
|
||||||
err := row.Scan(&member.Membership, &member.Displayname, &member.AvatarURL)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
s.log.Warnfln("Failed to scan member info of %s in %s: %v", userID, roomID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &member, err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) {
|
|
||||||
query := "INSERT INTO mx_user_profile (room_id, user_id, membership)" +
|
|
||||||
" VALUES ($1, $2, $3) ON CONFLICT (room_id, user_id) DO UPDATE SET" +
|
|
||||||
" membership=excluded.membership"
|
|
||||||
|
|
||||||
_, err := s.db.Exec(query, roomID, userID, membership)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warnfln("Failed to set membership of %s in %s to %s: %v", userID, roomID, membership, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) {
|
|
||||||
query := "INSERT INTO mx_user_profile" +
|
|
||||||
" (room_id, user_id, membership, displayname, avatar_url)" +
|
|
||||||
" VALUES ($1, $2, $3, $4, $5) ON CONFLICT (room_id, user_id)" +
|
|
||||||
" DO UPDATE SET membership=excluded.membership," +
|
|
||||||
" displayname=excluded.displayname, avatar_url=excluded.avatar_url"
|
|
||||||
_, err := s.db.Exec(query, roomID, userID, member.Membership, member.Displayname, member.AvatarURL)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warnfln("Failed to set membership of %s in %s to %s: %v", userID, roomID, member, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) {
|
|
||||||
levelsBytes, err := json.Marshal(levels)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorfln("Failed to marshal power levels of %s: %v", roomID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "INSERT INTO mx_room_state (room_id, power_levels)" +
|
|
||||||
" VALUES ($1, $2) ON CONFLICT (room_id) DO UPDATE SET" +
|
|
||||||
" power_levels=excluded.power_levels"
|
|
||||||
_, err = s.db.Exec(query, roomID, levelsBytes)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warnfln("Failed to store power levels of %s: %v", roomID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) GetPowerLevels(roomID id.RoomID) *event.PowerLevelsEventContent {
|
|
||||||
query := "SELECT power_levels FROM mx_room_state WHERE room_id=$1"
|
|
||||||
row := s.db.QueryRow(query, roomID)
|
|
||||||
if row == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []byte
|
|
||||||
err := row.Scan(&data)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorfln("Failed to scan power levels of %s: %v", roomID, err)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
levels := &event.PowerLevelsEventContent{}
|
|
||||||
err = json.Unmarshal(data, levels)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorfln("Failed to parse power levels of %s: %v", roomID, err)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return levels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int {
|
|
||||||
if s.db.dialect == "postgres" {
|
|
||||||
query := "SELECT COALESCE((power_levels->'users'->$2)::int," +
|
|
||||||
" (power_levels->'users_default')::int, 0)" +
|
|
||||||
" FROM mx_room_state WHERE room_id=$1"
|
|
||||||
row := s.db.QueryRow(query, roomID, userID)
|
|
||||||
if row == nil {
|
|
||||||
// Power levels not in db
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var powerLevel int
|
|
||||||
err := row.Scan(&powerLevel)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorfln("Failed to scan power level of %s in %s: %v", userID, roomID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return powerLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetPowerLevels(roomID).GetUserLevel(userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int {
|
|
||||||
if s.db.dialect == "postgres" {
|
|
||||||
defaultType := "events_default"
|
|
||||||
defaultValue := 0
|
|
||||||
if eventType.IsState() {
|
|
||||||
defaultType = "state_default"
|
|
||||||
defaultValue = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "SELECT COALESCE((power_levels->'events'->$2)::int," +
|
|
||||||
" (power_levels->'$3')::int, $4)" +
|
|
||||||
" FROM mx_room_state WHERE room_id=$1"
|
|
||||||
row := s.db.QueryRow(query, roomID, eventType.Type, defaultType, defaultValue)
|
|
||||||
if row == nil {
|
|
||||||
// Power levels not in db
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var powerLevel int
|
|
||||||
err := row.Scan(&powerLevel)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorfln("Failed to scan power level for %s in %s: %v", eventType, roomID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return powerLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetPowerLevels(roomID).GetEventLevel(eventType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool {
|
|
||||||
if s.db.dialect == "postgres" {
|
|
||||||
defaultType := "events_default"
|
|
||||||
defaultValue := 0
|
|
||||||
if eventType.IsState() {
|
|
||||||
defaultType = "state_default"
|
|
||||||
defaultValue = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "SELECT COALESCE((power_levels->'users'->$2)::int," +
|
|
||||||
" (power_levels->'users_default')::int, 0) >=" +
|
|
||||||
" COALESCE((power_levels->'events'->$3)::int," +
|
|
||||||
" (power_levels->'$4')::int, $5)" +
|
|
||||||
" FROM mx_room_state WHERE room_id=$1"
|
|
||||||
row := s.db.QueryRow(query, roomID, userID, eventType.Type, defaultType, defaultValue)
|
|
||||||
if row == nil {
|
|
||||||
// Power levels not in db
|
|
||||||
return defaultValue == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasPower bool
|
|
||||||
err := row.Scan(&hasPower)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorfln("Failed to scan power level for %s in %s: %v", eventType, roomID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasPower
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetPowerLevel(roomID, userID) >= s.GetPowerLevelRequirement(roomID, eventType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *SQLStateStore) FindSharedRooms(userID id.UserID) []id.RoomID {
|
|
||||||
query := `
|
|
||||||
SELECT room_id FROM mx_user_profile
|
|
||||||
LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id
|
|
||||||
WHERE user_id=$1 AND portal.encrypted=true
|
|
||||||
`
|
|
||||||
|
|
||||||
rooms := []id.RoomID{}
|
|
||||||
|
|
||||||
rows, err := store.db.Query(query, userID)
|
|
||||||
if err != nil {
|
|
||||||
store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var roomID id.RoomID
|
|
||||||
|
|
||||||
err = rows.Scan(&roomID)
|
|
||||||
if err != nil {
|
|
||||||
store.log.Warnfln("Failed to scan room ID: %v", err)
|
|
||||||
} else {
|
|
||||||
rooms = append(rooms, roomID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
}
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
-- v1: Initial revision
|
||||||
|
|
||||||
CREATE TABLE portal (
|
CREATE TABLE portal (
|
||||||
channel_id TEXT,
|
channel_id TEXT,
|
||||||
receiver TEXT,
|
receiver TEXT,
|
||||||
|
@ -9,6 +11,8 @@ CREATE TABLE portal (
|
||||||
avatar TEXT NOT NULL,
|
avatar TEXT NOT NULL,
|
||||||
avatar_url TEXT,
|
avatar_url TEXT,
|
||||||
|
|
||||||
|
encrypted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
type INT,
|
type INT,
|
||||||
dmuser TEXT,
|
dmuser TEXT,
|
||||||
|
|
||||||
|
@ -24,7 +28,12 @@ CREATE TABLE puppet (
|
||||||
avatar TEXT,
|
avatar TEXT,
|
||||||
avatar_url TEXT,
|
avatar_url TEXT,
|
||||||
|
|
||||||
enable_presence BOOLEAN NOT NULL DEFAULT true
|
enable_presence BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
enable_receipts BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
custom_mxid TEXT,
|
||||||
|
access_token TEXT,
|
||||||
|
next_batch TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE "user" (
|
CREATE TABLE "user" (
|
||||||
|
@ -38,12 +47,12 @@ CREATE TABLE "user" (
|
||||||
|
|
||||||
CREATE TABLE message (
|
CREATE TABLE message (
|
||||||
channel_id TEXT NOT NULL,
|
channel_id TEXT NOT NULL,
|
||||||
receiver TEXT NOT NULL,
|
receiver TEXT NOT NULL,
|
||||||
|
|
||||||
discord_message_id TEXT NOT NULL,
|
discord_message_id TEXT NOT NULL,
|
||||||
matrix_message_id TEXT NOT NULL UNIQUE,
|
matrix_message_id TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
author_id TEXT NOT NULL,
|
author_id TEXT NOT NULL,
|
||||||
timestamp BIGINT NOT NULL,
|
timestamp BIGINT NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY(discord_message_id, channel_id, receiver),
|
PRIMARY KEY(discord_message_id, channel_id, receiver),
|
||||||
|
@ -52,10 +61,10 @@ CREATE TABLE message (
|
||||||
|
|
||||||
CREATE TABLE reaction (
|
CREATE TABLE reaction (
|
||||||
channel_id TEXT NOT NULL,
|
channel_id TEXT NOT NULL,
|
||||||
receiver TEXT NOT NULL,
|
receiver TEXT NOT NULL,
|
||||||
|
|
||||||
discord_message_id TEXT NOT NULL,
|
discord_message_id TEXT NOT NULL,
|
||||||
matrix_event_id TEXT NOT NULL UNIQUE,
|
matrix_event_id TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
author_id TEXT NOT NULL,
|
author_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
@ -68,20 +77,29 @@ CREATE TABLE reaction (
|
||||||
FOREIGN KEY(channel_id, receiver) REFERENCES portal(channel_id, receiver) ON DELETE CASCADE
|
FOREIGN KEY(channel_id, receiver) REFERENCES portal(channel_id, receiver) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE mx_user_profile (
|
CREATE TABLE attachment (
|
||||||
room_id TEXT,
|
channel_id TEXT NOT NULL,
|
||||||
user_id TEXT,
|
receiver TEXT NOT NULL,
|
||||||
membership TEXT NOT NULL,
|
|
||||||
displayname TEXT,
|
discord_message_id TEXT NOT NULL,
|
||||||
avatar_url TEXT,
|
discord_attachment_id TEXT NOT NULL,
|
||||||
PRIMARY KEY (room_id, user_id)
|
|
||||||
|
matrix_event_id TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
PRIMARY KEY(discord_attachment_id, matrix_event_id),
|
||||||
|
FOREIGN KEY(channel_id, receiver) REFERENCES portal(channel_id, receiver) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE mx_registrations (
|
CREATE TABLE emoji (
|
||||||
user_id TEXT PRIMARY KEY
|
discord_id TEXT PRIMARY KEY,
|
||||||
|
discord_name TEXT,
|
||||||
|
matrix_url TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE mx_room_state (
|
CREATE TABLE guild (
|
||||||
room_id TEXT PRIMARY KEY,
|
discord_id TEXT NOT NULL,
|
||||||
power_levels TEXT
|
guild_id TEXT NOT NULL,
|
||||||
|
guild_name TEXT NOT NULL,
|
||||||
|
bridge BOOLEAN DEFAULT FALSE,
|
||||||
|
PRIMARY KEY(discord_id, guild_id)
|
||||||
);
|
);
|
32
database/upgrades/upgrades.go
Normal file
32
database/upgrades/upgrades.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// Copyright (C) 2022 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 upgrades
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Table dbutil.UpgradeTable
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var rawUpgrades embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Table.RegisterFS(rawUpgrades)
|
||||||
|
}
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
@ -19,10 +21,11 @@ type User struct {
|
||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Scan(row Scannable) *User {
|
func (u *User) Scan(row dbutil.Scannable) *User {
|
||||||
var token sql.NullString
|
var token sql.NullString
|
||||||
|
var discordID sql.NullString
|
||||||
|
|
||||||
err := row.Scan(&u.MXID, &u.ID, &u.ManagementRoom, &token)
|
err := row.Scan(&u.MXID, &discordID, &u.ManagementRoom, &token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
u.log.Errorln("Database scan failed:", err)
|
u.log.Errorln("Database scan failed:", err)
|
||||||
|
@ -35,6 +38,10 @@ func (u *User) Scan(row Scannable) *User {
|
||||||
u.Token = token.String
|
u.Token = token.String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if discordID.Valid {
|
||||||
|
u.ID = discordID.String
|
||||||
|
}
|
||||||
|
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,13 +51,19 @@ func (u *User) Insert() {
|
||||||
" VALUES ($1, $2, $3, $4);"
|
" VALUES ($1, $2, $3, $4);"
|
||||||
|
|
||||||
var token sql.NullString
|
var token sql.NullString
|
||||||
|
var discordID sql.NullString
|
||||||
|
|
||||||
if u.Token != "" {
|
if u.Token != "" {
|
||||||
token.String = u.Token
|
token.String = u.Token
|
||||||
token.Valid = true
|
token.Valid = true
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.db.Exec(query, u.MXID, u.ID, u.ManagementRoom, token)
|
if u.ID != "" {
|
||||||
|
discordID.String = u.ID
|
||||||
|
discordID.Valid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := u.db.Exec(query, u.MXID, discordID, u.ManagementRoom, token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Warnfln("Failed to insert %s: %v", u.MXID, err)
|
u.log.Warnfln("Failed to insert %s: %v", u.MXID, err)
|
||||||
|
@ -63,13 +76,19 @@ func (u *User) Update() {
|
||||||
" WHERE mxid=$4;"
|
" WHERE mxid=$4;"
|
||||||
|
|
||||||
var token sql.NullString
|
var token sql.NullString
|
||||||
|
var discordID sql.NullString
|
||||||
|
|
||||||
if u.Token != "" {
|
if u.Token != "" {
|
||||||
token.String = u.Token
|
token.String = u.Token
|
||||||
token.Valid = true
|
token.Valid = true
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.db.Exec(query, u.ID, u.ManagementRoom, token, u.MXID)
|
if u.ID != "" {
|
||||||
|
discordID.String = u.ID
|
||||||
|
discordID.Valid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := u.db.Exec(query, discordID, u.ManagementRoom, token, u.MXID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Warnfln("Failed to update %q: %v", u.MXID, err)
|
u.log.Warnfln("Failed to update %q: %v", u.MXID, err)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package bridge
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
|
@ -1,4 +1,4 @@
|
||||||
package bridge
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -10,7 +10,7 @@ import (
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Portal) downloadDiscordEmoji(id string, animated bool) ([]byte, string, error) {
|
func (portal *Portal) downloadDiscordEmoji(id string, animated bool) ([]byte, string, error) {
|
||||||
var url string
|
var url string
|
||||||
var mimeType string
|
var mimeType string
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ func (p *Portal) downloadDiscordEmoji(id string, animated bool) ([]byte, string,
|
||||||
return data, mimeType, err
|
return data, mimeType, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Portal) uploadMatrixEmoji(intent *appservice.IntentAPI, data []byte, mimeType string) (id.ContentURI, error) {
|
func (portal *Portal) uploadMatrixEmoji(intent *appservice.IntentAPI, data []byte, mimeType string) (id.ContentURI, error) {
|
||||||
uploaded, err := intent.UploadBytes(data, mimeType)
|
uploaded, err := intent.UploadBytes(data, mimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return id.ContentURI{}, err
|
return id.ContentURI{}, err
|
|
@ -11,6 +11,8 @@ homeserver:
|
||||||
# If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes.
|
# If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes.
|
||||||
# The bridge will use the appservice as_token to authorize requests.
|
# The bridge will use the appservice as_token to authorize requests.
|
||||||
status_endpoint: null
|
status_endpoint: null
|
||||||
|
# Endpoint for reporting per-message status.
|
||||||
|
message_send_checkpoint_endpoint: null
|
||||||
# Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?
|
# Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?
|
||||||
async_media: false
|
async_media: false
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ appservice:
|
||||||
# The hostname and port where this appservice should listen.
|
# The hostname and port where this appservice should listen.
|
||||||
hostname: 0.0.0.0
|
hostname: 0.0.0.0
|
||||||
port: 29334
|
port: 29334
|
||||||
|
|
||||||
# Database config.
|
# Database config.
|
||||||
database:
|
database:
|
||||||
# The database type. "sqlite3" and "postgres" are supported.
|
# The database type. "sqlite3" and "postgres" are supported.
|
||||||
|
@ -40,19 +43,16 @@ appservice:
|
||||||
max_conn_idle_time: null
|
max_conn_idle_time: null
|
||||||
max_conn_lifetime: null
|
max_conn_lifetime: null
|
||||||
|
|
||||||
# Settings for provisioning API
|
# The unique ID of this appservice.
|
||||||
provisioning:
|
|
||||||
# Prefix for the provisioning API paths.
|
|
||||||
prefix: /_matrix/provision
|
|
||||||
# Shared secret for authentication. If set to "generate", a random secret will be generated,
|
|
||||||
# or if set to "disable", the provisioning API will be disabled.
|
|
||||||
shared_secret: generate
|
|
||||||
|
|
||||||
id: discord
|
id: discord
|
||||||
|
# Appservice bot details.
|
||||||
bot:
|
bot:
|
||||||
username: discordbot
|
# Username of the appservice bot.
|
||||||
displayname: Discord bridge bot
|
username: discordbot
|
||||||
avatar: mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC
|
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
||||||
|
# to leave display name/avatar as-is.
|
||||||
|
displayname: Discord bridge bot
|
||||||
|
avatar: mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC
|
||||||
|
|
||||||
# Whether or not to receive ephemeral events via appservice transactions.
|
# Whether or not to receive ephemeral events via appservice transactions.
|
||||||
# Requires MSC2409 support (i.e. Synapse 1.22+).
|
# Requires MSC2409 support (i.e. Synapse 1.22+).
|
||||||
|
@ -71,6 +71,7 @@ bridge:
|
||||||
# Displayname template for Discord users.
|
# Displayname template for Discord users.
|
||||||
# TODO: document variables
|
# TODO: document variables
|
||||||
displayname_template: '{{.Username}}#{{.Discriminator}} (D){{if .Bot}} (bot){{end}}'
|
displayname_template: '{{.Username}}#{{.Discriminator}} (D){{if .Bot}} (bot){{end}}'
|
||||||
|
channelname_template: '{{if .Guild}}{{.Guild}} - {{end}}{{if .Folder}}{{.Folder}} - {{end}}{{.Name}} (D)'
|
||||||
|
|
||||||
portal_message_buffer: 128
|
portal_message_buffer: 128
|
||||||
|
|
||||||
|
@ -99,12 +100,12 @@ bridge:
|
||||||
example.com: foobar
|
example.com: foobar
|
||||||
|
|
||||||
# The prefix for commands. Only required in non-management rooms.
|
# The prefix for commands. Only required in non-management rooms.
|
||||||
command_prefix: '!dis'
|
command_prefix: '!discord'
|
||||||
# Messages sent upon joining a management room.
|
# Messages sent upon joining a management room.
|
||||||
# Markdown is supported. The defaults are listed below.
|
# Markdown is supported. The defaults are listed below.
|
||||||
management_room_text:
|
management_room_text:
|
||||||
# Sent when joining a room.
|
# Sent when joining a room.
|
||||||
welcome: "Hello, I'm a WhatsApp bridge bot."
|
welcome: "Hello, I'm a Discord bridge bot."
|
||||||
# Sent when joining a management room and the user is already logged in.
|
# Sent when joining a management room and the user is already logged in.
|
||||||
welcome_connected: "Use `help` for help."
|
welcome_connected: "Use `help` for help."
|
||||||
# Sent when joining a management room and the user is not logged in.
|
# Sent when joining a management room and the user is not logged in.
|
||||||
|
@ -135,6 +136,28 @@ bridge:
|
||||||
# Verification by the bridge is not yet implemented.
|
# Verification by the bridge is not yet implemented.
|
||||||
require_verification: true
|
require_verification: true
|
||||||
|
|
||||||
|
# Settings for provisioning API
|
||||||
|
provisioning:
|
||||||
|
# Prefix for the provisioning API paths.
|
||||||
|
prefix: /_matrix/provision
|
||||||
|
# Shared secret for authentication. If set to "generate", a random secret will be generated,
|
||||||
|
# or if set to "disable", the provisioning API will be disabled.
|
||||||
|
shared_secret: generate
|
||||||
|
|
||||||
|
# Permissions for using the bridge.
|
||||||
|
# Permitted values:
|
||||||
|
# relay - Talk through the relaybot (if enabled), no access otherwise
|
||||||
|
# user - Access to use the bridge to chat with a Discord account.
|
||||||
|
# admin - User level and some additional administration tools
|
||||||
|
# Permitted keys:
|
||||||
|
# * - All Matrix users
|
||||||
|
# domain - All users on that homeserver
|
||||||
|
# mxid - Specific user
|
||||||
|
permissions:
|
||||||
|
"*": relay
|
||||||
|
"example.com": user
|
||||||
|
"@admin:example.com": admin
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
directory: ./logs
|
directory: ./logs
|
||||||
file_name_format: '{{.Date}}-{{.Index}}.log'
|
file_name_format: '{{.Date}}-{{.Index}}.log'
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package globals
|
|
||||||
|
|
||||||
type Globals struct {
|
|
||||||
Config string `kong:"flag,name='config',short='c',env='CONFIG',help='The configuration file to use',default='config.yaml'"`
|
|
||||||
}
|
|
18
go.mod
18
go.mod
|
@ -3,30 +3,28 @@ module go.mau.fi/mautrix-discord
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v0.5.0
|
|
||||||
github.com/bwmarrin/discordgo v0.23.2
|
github.com/bwmarrin/discordgo v0.23.2
|
||||||
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.5
|
github.com/lib/pq v1.10.6
|
||||||
github.com/lopezator/migrator v0.3.0
|
github.com/mattn/go-sqlite3 v1.14.13
|
||||||
github.com/mattn/go-sqlite3 v1.14.12
|
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
|
||||||
maunium.net/go/maulogger/v2 v2.3.2
|
maunium.net/go/maulogger/v2 v2.3.2
|
||||||
maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417
|
maunium.net/go/mautrix v0.11.1-0.20220522190042-ec20c3fc994a
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/tidwall/gjson v1.14.1 // indirect
|
github.com/tidwall/gjson v1.14.1 // 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.4 // indirect
|
github.com/tidwall/sjson v1.2.4 // indirect
|
||||||
github.com/yuin/goldmark v1.4.12 // indirect
|
github.com/yuin/goldmark v1.4.12 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
|
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0 // indirect
|
||||||
|
maunium.net/go/mauflag v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/bwmarrin/discordgo v0.23.2 => gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7
|
replace github.com/bwmarrin/discordgo v0.23.2 => gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7
|
||||||
|
|
59
go.sum
59
go.sum
|
@ -1,42 +1,23 @@
|
||||||
github.com/alecthomas/kong v0.5.0 h1:u8Kdw+eeml93qtMZ04iei0CFYve/WPcA5IFh+9wSskE=
|
|
||||||
github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
|
|
||||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
|
||||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
|
||||||
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/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||||
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
|
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||||
github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
|
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|
||||||
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
|
|
||||||
github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/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=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
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/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
|
|
||||||
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
|
||||||
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
||||||
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
@ -45,47 +26,35 @@ 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.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
|
github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
|
||||||
github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
|
github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
|
||||||
github.com/yuin/goldmark v1.4.11 h1:i45YIzqLnUc2tGaTlJCyUxSG8TvgyGqhqOZOUKIjJ6w=
|
|
||||||
github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
|
||||||
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
|
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
|
||||||
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE=
|
gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE=
|
||||||
gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE=
|
gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE=
|
||||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
|
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
|
||||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
|
||||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0=
|
||||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM=
|
|
||||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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=
|
||||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||||
maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417 h1:dEJ9MKQvd4v2Rk2W6EUiO1T6PrSWPsB/JQOHQn4H6X0=
|
maunium.net/go/mautrix v0.11.1-0.20220522190042-ec20c3fc994a h1:hkr4xK3sXJv+WFAVAmpzBPbT2Q3bUn9S13QFIqzJgAw=
|
||||||
maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417/go.mod h1:zOor2zO/F10T/GbU67vWr0vnhLso88rlRr1HIrb1XWU=
|
maunium.net/go/mautrix v0.11.1-0.20220522190042-ec20c3fc994a/go.mod h1:CiKpMhAx5QZFHK03jpWb0iKI3sGU8x6+LfsOjDrcO8I=
|
||||||
|
|
181
main.go
181
main.go
|
@ -1,43 +1,168 @@
|
||||||
|
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
||||||
|
// Copyright (C) 2022 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
_ "embed"
|
||||||
"os"
|
"sync"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"go.mau.fi/mautrix-discord/database"
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/configupgrade"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/config"
|
"go.mau.fi/mautrix-discord/config"
|
||||||
"go.mau.fi/mautrix-discord/consts"
|
|
||||||
"go.mau.fi/mautrix-discord/globals"
|
|
||||||
"go.mau.fi/mautrix-discord/registration"
|
|
||||||
"go.mau.fi/mautrix-discord/run"
|
|
||||||
"go.mau.fi/mautrix-discord/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cli struct {
|
// Information to find out exactly which commit the bridge was built from.
|
||||||
globals.Globals
|
// These are filled at build time with the -X linker flag.
|
||||||
|
var (
|
||||||
|
Tag = "unknown"
|
||||||
|
Commit = "unknown"
|
||||||
|
BuildTime = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
GenerateConfig config.Cmd `kong:"cmd,help='Generate the default configuration and exit.'"`
|
//go:embed example-config.yaml
|
||||||
GenerateRegistration registration.Cmd `kong:"cmd,help='Generate the registration file for synapse and exit.'"`
|
var ExampleConfig string
|
||||||
Run run.Cmd `kong:"cmd,help='Run the bridge.',default='1'"`
|
|
||||||
Version version.Cmd `kong:"cmd,help='Display the version and exit.'"`
|
type DiscordBridge struct {
|
||||||
|
bridge.Bridge
|
||||||
|
|
||||||
|
Config *config.Config
|
||||||
|
DB *database.Database
|
||||||
|
|
||||||
|
provisioning *ProvisioningAPI
|
||||||
|
|
||||||
|
usersByMXID map[id.UserID]*User
|
||||||
|
usersByID map[string]*User
|
||||||
|
usersLock sync.Mutex
|
||||||
|
|
||||||
|
managementRooms map[id.RoomID]*User
|
||||||
|
managementRoomsLock sync.Mutex
|
||||||
|
|
||||||
|
portalsByMXID map[id.RoomID]*Portal
|
||||||
|
portalsByID map[database.PortalKey]*Portal
|
||||||
|
portalsLock sync.Mutex
|
||||||
|
|
||||||
|
puppets map[string]*Puppet
|
||||||
|
puppetsByCustomMXID map[id.UserID]*Puppet
|
||||||
|
puppetsLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetExampleConfig() string {
|
||||||
|
return ExampleConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetConfigPtr() interface{} {
|
||||||
|
br.Config = &config.Config{
|
||||||
|
BaseConfig: &br.Bridge.Config,
|
||||||
|
}
|
||||||
|
br.Config.BaseConfig.Bridge = &br.Config.Bridge
|
||||||
|
return br.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) Init() {
|
||||||
|
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
|
||||||
|
br.RegisterCommands()
|
||||||
|
|
||||||
|
br.DB = database.New(br.Bridge.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) Start() {
|
||||||
|
if br.Config.Bridge.Provisioning.SharedSecret != "disable" {
|
||||||
|
br.provisioning = newProvisioningAPI(br)
|
||||||
|
}
|
||||||
|
go br.startUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) Stop() {
|
||||||
|
for _, user := range br.usersByMXID {
|
||||||
|
if user.Session == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
br.Log.Debugln("Disconnecting", user.MXID)
|
||||||
|
user.Session.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetIPortal(mxid id.RoomID) bridge.Portal {
|
||||||
|
p := br.GetPortalByMXID(mxid)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetIUser(mxid id.UserID, create bool) bridge.User {
|
||||||
|
p := br.GetUserByMXID(mxid)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) IsGhost(mxid id.UserID) bool {
|
||||||
|
_, isGhost := br.ParsePuppetMXID(mxid)
|
||||||
|
return isGhost
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetIGhost(mxid id.UserID) bridge.Ghost {
|
||||||
|
p := br.GetPuppetByMXID(mxid)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) CreatePrivatePortal(id id.RoomID, user bridge.User, ghost bridge.Ghost) {
|
||||||
|
//TODO implement
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := kong.Parse(
|
br := &DiscordBridge{
|
||||||
&cli,
|
usersByMXID: make(map[id.UserID]*User),
|
||||||
kong.Name(consts.Name),
|
usersByID: make(map[string]*User),
|
||||||
kong.Description(consts.Description),
|
|
||||||
kong.UsageOnError(),
|
|
||||||
kong.ConfigureHelp(kong.HelpOptions{
|
|
||||||
Compact: true,
|
|
||||||
Summary: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
err := ctx.Run(&cli.Globals)
|
managementRooms: make(map[id.RoomID]*User),
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
portalsByMXID: make(map[id.RoomID]*Portal),
|
||||||
os.Exit(1)
|
portalsByID: make(map[database.PortalKey]*Portal),
|
||||||
|
|
||||||
|
puppets: make(map[string]*Puppet),
|
||||||
|
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
||||||
}
|
}
|
||||||
|
br.Bridge = bridge.Bridge{
|
||||||
|
Name: "mautrix-discord",
|
||||||
|
URL: "https://github.com/mautrix/discord",
|
||||||
|
Description: "A Matrix-Discord puppeting bridge.",
|
||||||
|
Version: "0.1.0",
|
||||||
|
ProtocolName: "Discord",
|
||||||
|
|
||||||
|
ConfigUpgrader: &configupgrade.StructUpgrader{
|
||||||
|
SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
|
||||||
|
Blocks: config.SpacedBlocks,
|
||||||
|
Base: ExampleConfig,
|
||||||
|
},
|
||||||
|
|
||||||
|
Child: br,
|
||||||
|
}
|
||||||
|
br.InitVersion(Tag, Commit, BuildTime)
|
||||||
|
|
||||||
|
br.Main()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package bridge
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
@ -25,21 +25,21 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProvisioningAPI struct {
|
type ProvisioningAPI struct {
|
||||||
bridge *Bridge
|
bridge *DiscordBridge
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProvisioningAPI(bridge *Bridge) *ProvisioningAPI {
|
func newProvisioningAPI(br *DiscordBridge) *ProvisioningAPI {
|
||||||
p := &ProvisioningAPI{
|
p := &ProvisioningAPI{
|
||||||
bridge: bridge,
|
bridge: br,
|
||||||
log: bridge.log.Sub("Provisioning"),
|
log: br.Log.Sub("Provisioning"),
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := bridge.Config.Appservice.Provisioning.Prefix
|
prefix := br.Config.Bridge.Provisioning.Prefix
|
||||||
|
|
||||||
p.log.Debugln("Enabling provisioning API at", prefix)
|
p.log.Debugln("Enabling provisioning API at", prefix)
|
||||||
|
|
||||||
r := bridge.as.Router.PathPrefix(prefix).Subrouter()
|
r := br.AS.Router.PathPrefix(prefix).Subrouter()
|
||||||
|
|
||||||
r.Use(p.authMiddleware)
|
r.Use(p.authMiddleware)
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ func (p *ProvisioningAPI) authMiddleware(h http.Handler) http.Handler {
|
||||||
auth = auth[len("Bearer "):]
|
auth = auth[len("Bearer "):]
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth != p.bridge.Config.Appservice.Provisioning.SharedSecret {
|
if auth != p.bridge.Config.Bridge.Provisioning.SharedSecret {
|
||||||
jsonResponse(w, http.StatusForbidden, map[string]interface{}{
|
jsonResponse(w, http.StatusForbidden, map[string]interface{}{
|
||||||
"error": "Invalid auth token",
|
"error": "Invalid auth token",
|
||||||
"errcode": "M_FORBIDDEN",
|
"errcode": "M_FORBIDDEN",
|
||||||
|
@ -176,7 +176,7 @@ func (p *ProvisioningAPI) ping(w http.ResponseWriter, r *http.Request) {
|
||||||
user := r.Context().Value("user").(*User)
|
user := r.Context().Value("user").(*User)
|
||||||
|
|
||||||
discord := map[string]interface{}{
|
discord := map[string]interface{}{
|
||||||
"logged_in": user.LoggedIn(),
|
"logged_in": user.IsLoggedIn(),
|
||||||
"connected": user.Connected(),
|
"connected": user.Connected(),
|
||||||
"conn": nil,
|
"conn": nil,
|
||||||
}
|
}
|
||||||
|
@ -210,7 +210,7 @@ func (p *ProvisioningAPI) logout(w http.ResponseWriter, r *http.Request) {
|
||||||
user := r.Context().Value("user").(*User)
|
user := r.Context().Value("user").(*User)
|
||||||
force := strings.ToLower(r.URL.Query().Get("force")) != "false"
|
force := strings.ToLower(r.URL.Query().Get("force")) != "false"
|
||||||
|
|
||||||
if !user.LoggedIn() {
|
if !user.IsLoggedIn() {
|
||||||
jsonResponse(w, http.StatusNotFound, Error{
|
jsonResponse(w, http.StatusNotFound, Error{
|
||||||
Error: "You're not logged in",
|
Error: "You're not logged in",
|
||||||
ErrCode: "not logged in",
|
ErrCode: "not logged in",
|
||||||
|
@ -285,7 +285,7 @@ func (p *ProvisioningAPI) login(w http.ResponseWriter, r *http.Request) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if user.LoggedIn() {
|
if user.IsLoggedIn() {
|
||||||
c.WriteJSON(Error{
|
c.WriteJSON(Error{
|
||||||
Error: "You're already logged into Discord",
|
Error: "You're already logged into Discord",
|
||||||
ErrCode: "already logged in",
|
ErrCode: "already logged in",
|
299
puppet.go
Normal file
299
puppet.go
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-discord/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Puppet struct {
|
||||||
|
*database.Puppet
|
||||||
|
|
||||||
|
bridge *DiscordBridge
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
|
MXID id.UserID
|
||||||
|
|
||||||
|
customIntent *appservice.IntentAPI
|
||||||
|
customUser *User
|
||||||
|
|
||||||
|
syncLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ bridge.Ghost = (*Puppet)(nil)
|
||||||
|
|
||||||
|
func (puppet *Puppet) GetMXID() id.UserID {
|
||||||
|
return puppet.MXID
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIDRegex *regexp.Regexp
|
||||||
|
|
||||||
|
func (br *DiscordBridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
|
||||||
|
return &Puppet{
|
||||||
|
Puppet: dbPuppet,
|
||||||
|
bridge: br,
|
||||||
|
log: br.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.ID)),
|
||||||
|
|
||||||
|
MXID: br.FormatPuppetMXID(dbPuppet.ID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) ParsePuppetMXID(mxid id.UserID) (string, bool) {
|
||||||
|
if userIDRegex == nil {
|
||||||
|
pattern := fmt.Sprintf(
|
||||||
|
"^@%s:%s$",
|
||||||
|
br.Config.Bridge.FormatUsername("([0-9]+)"),
|
||||||
|
br.Config.Homeserver.Domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
userIDRegex = regexp.MustCompile(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
match := userIDRegex.FindStringSubmatch(string(mxid))
|
||||||
|
if len(match) == 2 {
|
||||||
|
return match[1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
|
||||||
|
id, ok := br.ParsePuppetMXID(mxid)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return br.GetPuppetByID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetPuppetByID(id string) *Puppet {
|
||||||
|
br.puppetsLock.Lock()
|
||||||
|
defer br.puppetsLock.Unlock()
|
||||||
|
|
||||||
|
puppet, ok := br.puppets[id]
|
||||||
|
if !ok {
|
||||||
|
dbPuppet := br.DB.Puppet.Get(id)
|
||||||
|
if dbPuppet == nil {
|
||||||
|
dbPuppet = br.DB.Puppet.New()
|
||||||
|
dbPuppet.ID = id
|
||||||
|
dbPuppet.Insert()
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet = br.NewPuppet(dbPuppet)
|
||||||
|
br.puppets[puppet.ID] = puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
return puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet {
|
||||||
|
br.puppetsLock.Lock()
|
||||||
|
defer br.puppetsLock.Unlock()
|
||||||
|
|
||||||
|
puppet, ok := br.puppetsByCustomMXID[mxid]
|
||||||
|
if !ok {
|
||||||
|
dbPuppet := br.DB.Puppet.GetByCustomMXID(mxid)
|
||||||
|
if dbPuppet == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet = br.NewPuppet(dbPuppet)
|
||||||
|
br.puppets[puppet.ID] = puppet
|
||||||
|
br.puppetsByCustomMXID[puppet.CustomMXID] = puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
return puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetAllPuppetsWithCustomMXID() []*Puppet {
|
||||||
|
return br.dbPuppetsToPuppets(br.DB.Puppet.GetAllWithCustomMXID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetAllPuppets() []*Puppet {
|
||||||
|
return br.dbPuppetsToPuppets(br.DB.Puppet.GetAll())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet {
|
||||||
|
br.puppetsLock.Lock()
|
||||||
|
defer br.puppetsLock.Unlock()
|
||||||
|
|
||||||
|
output := make([]*Puppet, len(dbPuppets))
|
||||||
|
for index, dbPuppet := range dbPuppets {
|
||||||
|
if dbPuppet == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet, ok := br.puppets[dbPuppet.ID]
|
||||||
|
if !ok {
|
||||||
|
puppet = br.NewPuppet(dbPuppet)
|
||||||
|
br.puppets[dbPuppet.ID] = puppet
|
||||||
|
|
||||||
|
if dbPuppet.CustomMXID != "" {
|
||||||
|
br.puppetsByCustomMXID[dbPuppet.CustomMXID] = puppet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output[index] = puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) FormatPuppetMXID(did string) id.UserID {
|
||||||
|
return id.NewUserID(
|
||||||
|
br.Config.Bridge.FormatUsername(did),
|
||||||
|
br.Config.Homeserver.Domain,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
|
||||||
|
return puppet.bridge.AS.Intent(puppet.MXID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
|
||||||
|
if puppet.customIntent == nil {
|
||||||
|
return puppet.DefaultIntent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return puppet.customIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) CustomIntent() *appservice.IntentAPI {
|
||||||
|
return puppet.customIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) {
|
||||||
|
for _, portal := range puppet.bridge.GetAllPortalsByID(puppet.ID) {
|
||||||
|
// Get room create lock to prevent races between receiving contact info and room creation.
|
||||||
|
portal.roomCreateLock.Lock()
|
||||||
|
meta(portal)
|
||||||
|
portal.roomCreateLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updateName(source *User) bool {
|
||||||
|
user, err := source.Session.User(puppet.ID)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warnln("failed to get user from id:", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
newName := puppet.bridge.Config.Bridge.FormatDisplayname(user)
|
||||||
|
|
||||||
|
if puppet.DisplayName != newName {
|
||||||
|
err := puppet.DefaultIntent().SetDisplayName(newName)
|
||||||
|
if err == nil {
|
||||||
|
puppet.DisplayName = newName
|
||||||
|
go puppet.updatePortalName()
|
||||||
|
puppet.Update()
|
||||||
|
} else {
|
||||||
|
puppet.log.Warnln("failed to set display name:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updatePortalName() {
|
||||||
|
puppet.updatePortalMeta(func(portal *Portal) {
|
||||||
|
if portal.MXID != "" {
|
||||||
|
_, err := portal.MainIntent().SetRoomName(portal.MXID, puppet.DisplayName)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Warnln("Failed to set name:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.Name = puppet.DisplayName
|
||||||
|
portal.Update()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updateAvatar(source *User) bool {
|
||||||
|
user, err := source.Session.User(puppet.ID)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warnln("Failed to get user:", err)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if puppet.Avatar == user.Avatar {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Avatar == "" {
|
||||||
|
puppet.log.Warnln("User does not have an avatar")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := uploadAvatar(puppet.DefaultIntent(), user.AvatarURL(""))
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warnln("Failed to upload user avatar:", err)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.AvatarURL = url
|
||||||
|
|
||||||
|
err = puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warnln("Failed to set avatar:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.log.Debugln("Updated avatar", puppet.Avatar, "->", user.Avatar)
|
||||||
|
puppet.Avatar = user.Avatar
|
||||||
|
go puppet.updatePortalAvatar()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updatePortalAvatar() {
|
||||||
|
puppet.updatePortalMeta(func(portal *Portal) {
|
||||||
|
if portal.MXID != "" {
|
||||||
|
_, err := portal.MainIntent().SetRoomAvatar(portal.MXID, puppet.AvatarURL)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Warnln("Failed to set avatar:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.AvatarURL = puppet.AvatarURL
|
||||||
|
portal.Avatar = puppet.Avatar
|
||||||
|
portal.Update()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) SyncContact(source *User) {
|
||||||
|
puppet.syncLock.Lock()
|
||||||
|
defer puppet.syncLock.Unlock()
|
||||||
|
|
||||||
|
puppet.log.Debugln("syncing contact", puppet.DisplayName)
|
||||||
|
|
||||||
|
err := puppet.DefaultIntent().EnsureRegistered()
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Errorln("Failed to ensure registered:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := false
|
||||||
|
|
||||||
|
update = puppet.updateName(source) || update
|
||||||
|
|
||||||
|
if puppet.Avatar == "" {
|
||||||
|
update = puppet.updateAvatar(source) || update
|
||||||
|
puppet.log.Debugln("update avatar returned", update)
|
||||||
|
}
|
||||||
|
|
||||||
|
if update {
|
||||||
|
puppet.Update()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
package registration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix/appservice"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/config"
|
|
||||||
"go.mau.fi/mautrix-discord/globals"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cmd struct {
|
|
||||||
Filename string `kong:"flag,help='The filename to store the registration into',name='REGISTRATION',short='r',default='registration.yaml'"`
|
|
||||||
Force bool `kong:"flag,help='Overwrite an existing registration file if it already exists',short='f',default='0'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cmd) Run(g *globals.Globals) error {
|
|
||||||
// Check if the file exists before blinding overwriting it.
|
|
||||||
if _, err := os.Stat(c.Filename); err == nil {
|
|
||||||
if c.Force == false {
|
|
||||||
return fmt.Errorf("file %q exists, use -f to overwrite", c.Filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.FromFile(g.Config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
registration := appservice.CreateRegistration()
|
|
||||||
|
|
||||||
// Load existing values from the config into the registration.
|
|
||||||
if err := cfg.CopyToRegistration(registration); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the new App and Server tokens in the config.
|
|
||||||
cfg.Appservice.ASToken = registration.AppToken
|
|
||||||
cfg.Appservice.HSToken = registration.ServerToken
|
|
||||||
|
|
||||||
// Workaround for https://github.com/matrix-org/synapse/pull/5758
|
|
||||||
registration.SenderLocalpart = appservice.RandomString(32)
|
|
||||||
|
|
||||||
// Register the bot's user.
|
|
||||||
pattern := fmt.Sprintf(
|
|
||||||
"^@%s:%s$",
|
|
||||||
cfg.Appservice.Bot.Username,
|
|
||||||
cfg.Homeserver.Domain,
|
|
||||||
)
|
|
||||||
botRegex, err := regexp.Compile(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
registration.Namespaces.RegisterUserIDs(botRegex, true)
|
|
||||||
|
|
||||||
// Finally save the registration and the updated config file.
|
|
||||||
if err := registration.Save(c.Filename); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.Save(g.Config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
39
run/cmd.go
39
run/cmd.go
|
@ -1,39 +0,0 @@
|
||||||
package run
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/bridge"
|
|
||||||
"go.mau.fi/mautrix-discord/config"
|
|
||||||
"go.mau.fi/mautrix-discord/globals"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cmd struct{}
|
|
||||||
|
|
||||||
func (c *Cmd) Run(g *globals.Globals) error {
|
|
||||||
fmt.Printf("g.Config: %q\n", g.Config)
|
|
||||||
cfg, err := config.FromFile(g.Config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge, err := bridge.New(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan os.Signal)
|
|
||||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
|
||||||
<-ch
|
|
||||||
|
|
||||||
bridge.Stop()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
820
user.go
Normal file
820
user.go
Normal file
|
@ -0,0 +1,820 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-discord/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotConnected = errors.New("not connected")
|
||||||
|
ErrNotLoggedIn = errors.New("not logged in")
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
*database.User
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
bridge *DiscordBridge
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
|
PermissionLevel bridgeconfig.PermissionLevel
|
||||||
|
|
||||||
|
guilds map[string]*database.Guild
|
||||||
|
guildsLock sync.Mutex
|
||||||
|
|
||||||
|
Session *discordgo.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetPermissionLevel() bridgeconfig.PermissionLevel {
|
||||||
|
return user.PermissionLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetManagementRoomID() id.RoomID {
|
||||||
|
return user.ManagementRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetMXID() id.UserID {
|
||||||
|
return user.MXID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetCommandState() map[string]interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetIDoublePuppet() bridge.DoublePuppet {
|
||||||
|
p := user.bridge.GetPuppetByCustomMXID(user.MXID)
|
||||||
|
if p == nil || p.CustomIntent() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetIGhost() bridge.Ghost {
|
||||||
|
if user.ID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p := user.bridge.GetPuppetByID(user.ID)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ bridge.User = (*User)(nil)
|
||||||
|
|
||||||
|
// this assume you are holding the guilds lock!!!
|
||||||
|
func (user *User) loadGuilds() {
|
||||||
|
user.guilds = map[string]*database.Guild{}
|
||||||
|
for _, guild := range user.bridge.DB.Guild.GetAll(user.ID) {
|
||||||
|
user.guilds[guild.GuildID] = guild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) loadUser(dbUser *database.User, mxid *id.UserID) *User {
|
||||||
|
// If we weren't passed in a user we attempt to create one if we were given
|
||||||
|
// a matrix id.
|
||||||
|
if dbUser == nil {
|
||||||
|
if mxid == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser = br.DB.User.New()
|
||||||
|
dbUser.MXID = *mxid
|
||||||
|
dbUser.Insert()
|
||||||
|
}
|
||||||
|
|
||||||
|
user := br.NewUser(dbUser)
|
||||||
|
|
||||||
|
// We assume the usersLock was acquired by our caller.
|
||||||
|
br.usersByMXID[user.MXID] = user
|
||||||
|
if user.ID != "" {
|
||||||
|
br.usersByID[user.ID] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.ManagementRoom != "" {
|
||||||
|
// Lock the management rooms for our update
|
||||||
|
br.managementRoomsLock.Lock()
|
||||||
|
br.managementRooms[user.ManagementRoom] = user
|
||||||
|
br.managementRoomsLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load our guilds state from the database and turn it into a map
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
user.loadGuilds()
|
||||||
|
user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetUserByMXID(userID id.UserID) *User {
|
||||||
|
// TODO: check if puppet
|
||||||
|
|
||||||
|
br.usersLock.Lock()
|
||||||
|
defer br.usersLock.Unlock()
|
||||||
|
|
||||||
|
user, ok := br.usersByMXID[userID]
|
||||||
|
if !ok {
|
||||||
|
return br.loadUser(br.DB.User.GetByMXID(userID), &userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) GetUserByID(id string) *User {
|
||||||
|
br.usersLock.Lock()
|
||||||
|
defer br.usersLock.Unlock()
|
||||||
|
|
||||||
|
user, ok := br.usersByID[id]
|
||||||
|
if !ok {
|
||||||
|
return br.loadUser(br.DB.User.GetByID(id), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) NewUser(dbUser *database.User) *User {
|
||||||
|
user := &User{
|
||||||
|
User: dbUser,
|
||||||
|
bridge: br,
|
||||||
|
log: br.Log.Sub("User").Sub(string(dbUser.MXID)),
|
||||||
|
guilds: map[string]*database.Guild{},
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PermissionLevel = br.Config.Bridge.Permissions.Get(user.MXID)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) getAllUsers() []*User {
|
||||||
|
br.usersLock.Lock()
|
||||||
|
defer br.usersLock.Unlock()
|
||||||
|
|
||||||
|
dbUsers := br.DB.User.GetAll()
|
||||||
|
users := make([]*User, len(dbUsers))
|
||||||
|
|
||||||
|
for idx, dbUser := range dbUsers {
|
||||||
|
user, ok := br.usersByMXID[dbUser.MXID]
|
||||||
|
if !ok {
|
||||||
|
user = br.loadUser(dbUser, nil)
|
||||||
|
}
|
||||||
|
users[idx] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) startUsers() {
|
||||||
|
br.Log.Debugln("Starting users")
|
||||||
|
|
||||||
|
for _, user := range br.getAllUsers() {
|
||||||
|
go user.Connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
br.Log.Debugln("Starting custom puppets")
|
||||||
|
for _, customPuppet := range br.GetAllPuppetsWithCustomMXID() {
|
||||||
|
go func(puppet *Puppet) {
|
||||||
|
br.Log.Debugln("Starting custom puppet", puppet.CustomMXID)
|
||||||
|
|
||||||
|
if err := puppet.StartCustomMXID(true); err != nil {
|
||||||
|
puppet.log.Errorln("Failed to start custom puppet:", err)
|
||||||
|
}
|
||||||
|
}(customPuppet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SetManagementRoom(roomID id.RoomID) {
|
||||||
|
user.bridge.managementRoomsLock.Lock()
|
||||||
|
defer user.bridge.managementRoomsLock.Unlock()
|
||||||
|
|
||||||
|
existing, ok := user.bridge.managementRooms[roomID]
|
||||||
|
if ok {
|
||||||
|
// If there's a user already assigned to this management room, clear it
|
||||||
|
// out.
|
||||||
|
// I think this is due a name change or something? I dunno, leaving it
|
||||||
|
// for now.
|
||||||
|
existing.ManagementRoom = ""
|
||||||
|
existing.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ManagementRoom = roomID
|
||||||
|
user.bridge.managementRooms[user.ManagementRoom] = user
|
||||||
|
user.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) tryAutomaticDoublePuppeting() {
|
||||||
|
user.Lock()
|
||||||
|
defer user.Unlock()
|
||||||
|
|
||||||
|
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Debugln("Checking if double puppeting needs to be enabled")
|
||||||
|
|
||||||
|
puppet := user.bridge.GetPuppetByID(user.ID)
|
||||||
|
if puppet.CustomMXID != "" {
|
||||||
|
user.log.Debugln("User already has double-puppeting enabled")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := puppet.loginWithSharedSecret(user.MXID)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnln("Failed to login with shared secret:", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = puppet.SwitchCustomMXID(accessToken, user.MXID)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Infoln("Successfully automatically enabled custom puppet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
|
||||||
|
doublePuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
|
||||||
|
if doublePuppet == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if doublePuppet == nil || doublePuppet.CustomIntent() == nil || portal.MXID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO sync mute status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Login(token string) error {
|
||||||
|
user.Token = token
|
||||||
|
user.Update()
|
||||||
|
return user.Connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) IsLoggedIn() bool {
|
||||||
|
user.Lock()
|
||||||
|
defer user.Unlock()
|
||||||
|
|
||||||
|
return user.Token != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Logout() error {
|
||||||
|
user.Lock()
|
||||||
|
defer user.Unlock()
|
||||||
|
|
||||||
|
if user.Session == nil {
|
||||||
|
return ErrNotLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet := user.bridge.GetPuppetByID(user.ID)
|
||||||
|
if puppet.CustomMXID != "" {
|
||||||
|
err := puppet.SwitchCustomMXID("", "")
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnln("Failed to logout-matrix while logging out of Discord:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Session.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Session = nil
|
||||||
|
|
||||||
|
user.Token = ""
|
||||||
|
user.Update()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Connected() bool {
|
||||||
|
user.Lock()
|
||||||
|
defer user.Unlock()
|
||||||
|
|
||||||
|
return user.Session != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Connect() error {
|
||||||
|
user.Lock()
|
||||||
|
defer user.Unlock()
|
||||||
|
|
||||||
|
if user.Token == "" {
|
||||||
|
return ErrNotLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Debugln("connecting to discord")
|
||||||
|
|
||||||
|
session, err := discordgo.New(user.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Session = session
|
||||||
|
|
||||||
|
// Add our event handlers
|
||||||
|
user.Session.AddHandler(user.readyHandler)
|
||||||
|
user.Session.AddHandler(user.connectedHandler)
|
||||||
|
user.Session.AddHandler(user.disconnectedHandler)
|
||||||
|
|
||||||
|
user.Session.AddHandler(user.guildCreateHandler)
|
||||||
|
user.Session.AddHandler(user.guildDeleteHandler)
|
||||||
|
user.Session.AddHandler(user.guildUpdateHandler)
|
||||||
|
|
||||||
|
user.Session.AddHandler(user.channelCreateHandler)
|
||||||
|
user.Session.AddHandler(user.channelDeleteHandler)
|
||||||
|
user.Session.AddHandler(user.channelPinsUpdateHandler)
|
||||||
|
user.Session.AddHandler(user.channelUpdateHandler)
|
||||||
|
|
||||||
|
user.Session.AddHandler(user.messageCreateHandler)
|
||||||
|
user.Session.AddHandler(user.messageDeleteHandler)
|
||||||
|
user.Session.AddHandler(user.messageUpdateHandler)
|
||||||
|
user.Session.AddHandler(user.reactionAddHandler)
|
||||||
|
user.Session.AddHandler(user.reactionRemoveHandler)
|
||||||
|
|
||||||
|
user.Session.Identify.Presence.Status = "online"
|
||||||
|
|
||||||
|
return user.Session.Open()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Disconnect() error {
|
||||||
|
user.Lock()
|
||||||
|
defer user.Unlock()
|
||||||
|
|
||||||
|
if user.Session == nil {
|
||||||
|
return ErrNotConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Session.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Session = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) bridgeMessage(guildID string) bool {
|
||||||
|
// Non guild message always get bridged.
|
||||||
|
if guildID == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
if guild, found := user.guilds[guildID]; found {
|
||||||
|
if guild.Bridge {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Debugfln("ignoring message for non-bridged guild %s-%s", user.ID, guildID)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) readyHandler(s *discordgo.Session, r *discordgo.Ready) {
|
||||||
|
user.log.Debugln("discord connection ready")
|
||||||
|
|
||||||
|
// Update our user fields
|
||||||
|
user.ID = r.User.ID
|
||||||
|
|
||||||
|
// Update our guild map to match watch discord thinks we're in. This is the
|
||||||
|
// only time we can get the full guild map as discordgo doesn't make it
|
||||||
|
// available to us later. Also, discord might not give us the full guild
|
||||||
|
// information here, so we use this to remove guilds the user left and only
|
||||||
|
// add guilds whose full information we have. The are told about the
|
||||||
|
// "unavailable" guilds later via the GuildCreate handler.
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
// build a list of the current guilds we're in so we can prune the old ones
|
||||||
|
current := []string{}
|
||||||
|
|
||||||
|
user.log.Debugln("database guild count", len(user.guilds))
|
||||||
|
user.log.Debugln("discord guild count", len(r.Guilds))
|
||||||
|
|
||||||
|
for _, guild := range r.Guilds {
|
||||||
|
current = append(current, guild.ID)
|
||||||
|
|
||||||
|
// If we already know about this guild, make sure we reset it's bridge
|
||||||
|
// status.
|
||||||
|
if val, found := user.guilds[guild.ID]; found {
|
||||||
|
bridge := val.Bridge
|
||||||
|
user.guilds[guild.ID].Bridge = bridge
|
||||||
|
|
||||||
|
// Update the name if the guild is available
|
||||||
|
if !guild.Unavailable {
|
||||||
|
user.guilds[guild.ID].GuildName = guild.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
val.Upsert()
|
||||||
|
} else {
|
||||||
|
g := user.bridge.DB.Guild.New()
|
||||||
|
g.DiscordID = user.ID
|
||||||
|
g.GuildID = guild.ID
|
||||||
|
user.guilds[guild.ID] = g
|
||||||
|
|
||||||
|
if !guild.Unavailable {
|
||||||
|
g.GuildName = guild.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Upsert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync the guilds to the database.
|
||||||
|
user.bridge.DB.Guild.Prune(user.ID, current)
|
||||||
|
|
||||||
|
// Finally reload from the database since it purged servers we're not in
|
||||||
|
// anymore.
|
||||||
|
user.loadGuilds()
|
||||||
|
|
||||||
|
user.log.Debugln("updated database guild count", len(user.guilds))
|
||||||
|
|
||||||
|
user.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) connectedHandler(s *discordgo.Session, c *discordgo.Connect) {
|
||||||
|
user.log.Debugln("connected to discord")
|
||||||
|
|
||||||
|
user.tryAutomaticDoublePuppeting()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) disconnectedHandler(s *discordgo.Session, d *discordgo.Disconnect) {
|
||||||
|
user.log.Debugln("disconnected from discord")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) guildCreateHandler(s *discordgo.Session, g *discordgo.GuildCreate) {
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
// If we somehow already know about the guild, just update it's name
|
||||||
|
if guild, found := user.guilds[g.ID]; found {
|
||||||
|
guild.GuildName = g.Name
|
||||||
|
guild.Upsert()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a brand new guild so lets get it added.
|
||||||
|
guild := user.bridge.DB.Guild.New()
|
||||||
|
guild.DiscordID = user.ID
|
||||||
|
guild.GuildID = g.ID
|
||||||
|
guild.GuildName = g.Name
|
||||||
|
guild.Upsert()
|
||||||
|
|
||||||
|
user.guilds[g.ID] = guild
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) guildDeleteHandler(s *discordgo.Session, g *discordgo.GuildDelete) {
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
if guild, found := user.guilds[g.ID]; found {
|
||||||
|
guild.Delete()
|
||||||
|
delete(user.guilds, g.ID)
|
||||||
|
user.log.Debugln("deleted guild", g.Guild.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) guildUpdateHandler(s *discordgo.Session, g *discordgo.GuildUpdate) {
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
// If we somehow already know about the guild, just update it's name
|
||||||
|
if guild, found := user.guilds[g.ID]; found {
|
||||||
|
guild.GuildName = g.Name
|
||||||
|
guild.Upsert()
|
||||||
|
|
||||||
|
user.log.Debugln("updated guild", g.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) createChannel(c *discordgo.Channel) {
|
||||||
|
key := database.NewPortalKey(c.ID, user.User.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
if portal.MXID != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.Name = c.Name
|
||||||
|
portal.Topic = c.Topic
|
||||||
|
portal.Type = c.Type
|
||||||
|
|
||||||
|
if portal.Type == discordgo.ChannelTypeDM {
|
||||||
|
portal.DMUser = c.Recipients[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Icon != "" {
|
||||||
|
user.log.Debugln("channel icon", c.Icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.Update()
|
||||||
|
|
||||||
|
portal.createMatrixRoom(user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) channelCreateHandler(s *discordgo.Session, c *discordgo.ChannelCreate) {
|
||||||
|
user.createChannel(c.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) channelDeleteHandler(s *discordgo.Session, c *discordgo.ChannelDelete) {
|
||||||
|
user.log.Debugln("channel delete handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) channelPinsUpdateHandler(s *discordgo.Session, c *discordgo.ChannelPinsUpdate) {
|
||||||
|
user.log.Debugln("channel pins update")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) channelUpdateHandler(s *discordgo.Session, c *discordgo.ChannelUpdate) {
|
||||||
|
key := database.NewPortalKey(c.ID, user.User.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
portal.update(user, c.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) messageCreateHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
if !user.bridgeMessage(m.GuildID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := database.NewPortalKey(m.ChannelID, user.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
msg := portalDiscordMessage{
|
||||||
|
msg: m,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.discordMessages <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) messageDeleteHandler(s *discordgo.Session, m *discordgo.MessageDelete) {
|
||||||
|
if !user.bridgeMessage(m.GuildID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := database.NewPortalKey(m.ChannelID, user.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
msg := portalDiscordMessage{
|
||||||
|
msg: m,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.discordMessages <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) messageUpdateHandler(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
||||||
|
if !user.bridgeMessage(m.GuildID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := database.NewPortalKey(m.ChannelID, user.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
msg := portalDiscordMessage{
|
||||||
|
msg: m,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.discordMessages <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) reactionAddHandler(s *discordgo.Session, m *discordgo.MessageReactionAdd) {
|
||||||
|
if !user.bridgeMessage(m.MessageReaction.GuildID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := database.NewPortalKey(m.ChannelID, user.User.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
msg := portalDiscordMessage{
|
||||||
|
msg: m,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.discordMessages <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) reactionRemoveHandler(s *discordgo.Session, m *discordgo.MessageReactionRemove) {
|
||||||
|
if !user.bridgeMessage(m.MessageReaction.GuildID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := database.NewPortalKey(m.ChannelID, user.User.ID)
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
|
||||||
|
msg := portalDiscordMessage{
|
||||||
|
msg: m,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.discordMessages <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) bool {
|
||||||
|
ret := false
|
||||||
|
|
||||||
|
inviteContent := event.Content{
|
||||||
|
Parsed: &event.MemberEventContent{
|
||||||
|
Membership: event.MembershipInvite,
|
||||||
|
IsDirect: isDirect,
|
||||||
|
},
|
||||||
|
Raw: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
customPuppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
|
||||||
|
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
||||||
|
inviteContent.Raw["fi.mau.will_auto_accept"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := intent.SendStateEvent(roomID, event.StateMember, user.MXID.String(), &inviteContent)
|
||||||
|
|
||||||
|
var httpErr mautrix.HTTPError
|
||||||
|
if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
|
||||||
|
user.bridge.StateStore.SetMembership(roomID, user.MXID, event.MembershipJoin)
|
||||||
|
ret = true
|
||||||
|
} else if err != nil {
|
||||||
|
user.log.Warnfln("Failed to invite user to %s: %v", roomID, err)
|
||||||
|
} else {
|
||||||
|
ret = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if customPuppet != nil && customPuppet.CustomIntent() != nil {
|
||||||
|
err = customPuppet.CustomIntent().EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnfln("Failed to auto-join %s: %v", roomID, err)
|
||||||
|
ret = false
|
||||||
|
} else {
|
||||||
|
ret = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) getDirectChats() map[id.UserID][]id.RoomID {
|
||||||
|
chats := map[id.UserID][]id.RoomID{}
|
||||||
|
|
||||||
|
privateChats := user.bridge.DB.Portal.FindPrivateChats(user.ID)
|
||||||
|
for _, portal := range privateChats {
|
||||||
|
if portal.MXID != "" {
|
||||||
|
puppetMXID := user.bridge.FormatPuppetMXID(portal.Key.Receiver)
|
||||||
|
|
||||||
|
chats[puppetMXID] = []id.RoomID{portal.MXID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) updateDirectChats(chats map[id.UserID][]id.RoomID) {
|
||||||
|
if !user.bridge.Config.Bridge.SyncDirectChatList {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet := user.bridge.GetPuppetByMXID(user.MXID)
|
||||||
|
if puppet == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
intent := puppet.CustomIntent()
|
||||||
|
if intent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
method := http.MethodPatch
|
||||||
|
if chats == nil {
|
||||||
|
chats = user.getDirectChats()
|
||||||
|
method = http.MethodPut
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Debugln("Updating m.direct list on homeserver")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if user.bridge.Config.Homeserver.Asmux {
|
||||||
|
urlPath := intent.BuildURL(mautrix.ClientURLPath{"unstable", "com.beeper.asmux", "dms"})
|
||||||
|
_, err = intent.MakeFullRequest(mautrix.FullRequest{
|
||||||
|
Method: method,
|
||||||
|
URL: urlPath,
|
||||||
|
Headers: http.Header{"X-Asmux-Auth": {user.bridge.AS.Registration.AppToken}},
|
||||||
|
RequestJSON: chats,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
existingChats := map[id.UserID][]id.RoomID{}
|
||||||
|
|
||||||
|
err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnln("Failed to get m.direct list to update it:", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for userID, rooms := range existingChats {
|
||||||
|
if _, ok := user.bridge.ParsePuppetMXID(userID); !ok {
|
||||||
|
// This is not a ghost user, include it in the new list
|
||||||
|
chats[userID] = rooms
|
||||||
|
} else if _, ok := chats[userID]; !ok && method == http.MethodPatch {
|
||||||
|
// This is a ghost user, but we're not replacing the whole list, so include it too
|
||||||
|
chats[userID] = rooms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnln("Failed to update m.direct list:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) bridgeGuild(guildID string, everything bool) error {
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
guild, found := user.guilds[guildID]
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("guildID not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the guild
|
||||||
|
guild.Bridge = true
|
||||||
|
guild.Upsert()
|
||||||
|
|
||||||
|
// If this is a full bridge, create portals for all the channels
|
||||||
|
if everything {
|
||||||
|
channels, err := user.Session.GuildChannels(guildID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channelIsBridgeable(channel) {
|
||||||
|
user.createChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) unbridgeGuild(guildID string) error {
|
||||||
|
user.guildsLock.Lock()
|
||||||
|
defer user.guildsLock.Unlock()
|
||||||
|
|
||||||
|
guild, exists := user.guilds[guildID]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("guildID not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !guild.Bridge {
|
||||||
|
return fmt.Errorf("guild not bridged")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First update the guild so we don't have any other go routines recreating
|
||||||
|
// channels we're about to destroy.
|
||||||
|
guild.Bridge = false
|
||||||
|
guild.Upsert()
|
||||||
|
|
||||||
|
// Now run through the channels in the guild and remove any portals we
|
||||||
|
// have for them.
|
||||||
|
channels, err := user.Session.GuildChannels(guildID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channelIsBridgeable(channel) {
|
||||||
|
key := database.PortalKey{
|
||||||
|
ChannelID: channel.ID,
|
||||||
|
Receiver: user.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
portal := user.bridge.GetPortalByID(key)
|
||||||
|
portal.leave(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
package version
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/consts"
|
|
||||||
"go.mau.fi/mautrix-discord/globals"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cmd struct{}
|
|
||||||
|
|
||||||
func (c *Cmd) Run(g *globals.Globals) error {
|
|
||||||
fmt.Printf("%s %s\n", consts.Name, String)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package version
|
|
||||||
|
|
||||||
const String = "0.0.1"
|
|
Loading…
Reference in a new issue