Add more options for guild message handling

This commit is contained in:
Tulir Asokan
2023-02-18 22:53:51 +02:00
parent 541c8e1169
commit 4676ec98c4
7 changed files with 152 additions and 39 deletions

View File

@@ -34,6 +34,7 @@ import (
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database"
"go.mau.fi/mautrix-discord/remoteauth" "go.mau.fi/mautrix-discord/remoteauth"
) )
@@ -305,18 +306,19 @@ var cmdGuilds = &commands.FullHandler{
Help: commands.HelpMeta{ Help: commands.HelpMeta{
Section: commands.HelpSectionUnclassified, Section: commands.HelpSectionUnclassified,
Description: "Guild bridging management", Description: "Guild bridging management",
Args: "<status/bridge/unbridge> [_guild ID_] [--entire]", Args: "<status/bridge/unbridge/bridging-mode> [_guild ID_] [...]",
}, },
RequiresLogin: true, RequiresLogin: true,
} }
const smallGuildsHelp = "**Usage**: `$cmdprefix guilds <help/status/bridge/unbridge> [guild ID] [--entire]`" const smallGuildsHelp = "**Usage**: `$cmdprefix guilds <help/status/bridge/unbridge> [guild ID] [...]`"
const fullGuildsHelp = smallGuildsHelp + ` const fullGuildsHelp = smallGuildsHelp + `
* **help** - View this help message. * **help** - View this help message.
* **status** - View the list of guilds and their bridging status. * **status** - View the list of guilds and their bridging status.
* **bridge <_guild ID_> [--entire]** - Enable bridging for a guild. The --entire flag auto-creates portals for all channels. * **bridge <_guild ID_> [--entire]** - Enable bridging for a guild. The --entire flag auto-creates portals for all channels.
* **bridging-mode <_guild ID_> <_mode_>** - Set the mode for bridging messages and new channels in a guild.
* **unbridge <_guild ID_>** - Unbridge a guild and delete all channel portal rooms.` * **unbridge <_guild ID_>** - Unbridge a guild and delete all channel portal rooms.`
func fnGuilds(ce *WrappedCommandEvent) { func fnGuilds(ce *WrappedCommandEvent) {
@@ -333,6 +335,8 @@ func fnGuilds(ce *WrappedCommandEvent) {
fnBridgeGuild(ce) fnBridgeGuild(ce)
case "unbridge", "delete": case "unbridge", "delete":
fnUnbridgeGuild(ce) fnUnbridgeGuild(ce)
case "bridging-mode", "mode":
fnGuildBridgingMode(ce)
case "help": case "help":
ce.Reply(fullGuildsHelp) ce.Reply(fullGuildsHelp)
default: default:
@@ -347,15 +351,11 @@ func fnListGuilds(ce *WrappedCommandEvent) {
if guild == nil { if guild == nil {
continue continue
} }
status := "not bridged"
if guild.MXID != "" {
status = "bridged"
}
var avatarHTML string var avatarHTML string
if !guild.AvatarURL.IsEmpty() { if !guild.AvatarURL.IsEmpty() {
avatarHTML = fmt.Sprintf(`<img data-mx-emoticon height="24" src="%s" alt="" title="Guild avatar"> `, guild.AvatarURL.String()) avatarHTML = fmt.Sprintf(`<img data-mx-emoticon height="24" src="%s" alt="" title="Guild avatar"> `, guild.AvatarURL.String())
} }
items = append(items, fmt.Sprintf("<li>%s%s (<code>%s</code>) - %s</li>", avatarHTML, html.EscapeString(guild.Name), guild.ID, status)) items = append(items, fmt.Sprintf("<li>%s%s (<code>%s</code>) - %s</li>", avatarHTML, html.EscapeString(guild.Name), guild.ID, guild.BridgingMode.Description()))
} }
if len(items) == 0 { if len(items) == 0 {
ce.Reply("No guilds found") ce.Reply("No guilds found")
@@ -384,6 +384,36 @@ func fnUnbridgeGuild(ce *WrappedCommandEvent) {
} }
} }
const availableModes = "Available modes:\n" +
"* `nothing` to never bridge any messages (default when unbridged)\n" +
"* `if-portal-exists` to bridge messages in existing portals, but drop messages in unbridged channels\n" +
"* `create-on-message` to bridge all messages and create portals if necessary on incoming messages (default after bridging)\n" +
"* `everything` to bridge all messages and create portals proactively on bridge startup (default if bridged with `--entire`)\n"
func fnGuildBridgingMode(ce *WrappedCommandEvent) {
if len(ce.Args) == 0 || len(ce.Args) > 2 {
ce.Reply("**Usage**: `$cmdprefix guilds bridging-mode <guild ID> [mode]`\n\n" + availableModes)
return
}
guild := ce.Bridge.GetGuildByID(ce.Args[0], false)
if guild == nil {
ce.Reply("Guild not found")
return
}
if len(ce.Args) == 1 {
ce.Reply("%s (%s) is currently set to %s (`%s`)\n\n%s", guild.PlainName, guild.ID, guild.BridgingMode.Description(), guild.BridgingMode.String(), availableModes)
return
}
mode := database.ParseGuildBridgingMode(ce.Args[1])
if mode == database.GuildBridgeInvalid {
ce.Reply("Invalid guild bridging mode `%s`", ce.Args[1])
return
}
guild.BridgingMode = mode
guild.Update()
ce.Reply("Set guild bridging mode to %s", mode.Description())
}
var cmdDeleteAllPortals = &commands.FullHandler{ var cmdDeleteAllPortals = &commands.FullHandler{
Func: wrapCommand(fnDeleteAllPortals), Func: wrapCommand(fnDeleteAllPortals),
Name: "delete-all-portals", Name: "delete-all-portals",

View File

@@ -3,6 +3,8 @@ package database
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"strings"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@@ -10,13 +12,76 @@ import (
"maunium.net/go/mautrix/util/dbutil" "maunium.net/go/mautrix/util/dbutil"
) )
type GuildBridgingMode int
const (
// GuildBridgeNothing tells the bridge to never bridge messages, not even checking if a portal exists.
GuildBridgeNothing GuildBridgingMode = iota
// GuildBridgeIfPortalExists tells the bridge to bridge messages in channels that already have portals.
GuildBridgeIfPortalExists
// GuildBridgeCreateOnMessage tells the bridge to create portals as soon as a message is received.
GuildBridgeCreateOnMessage
// GuildBridgeEverything tells the bridge to proactively create portals on startup and when receiving channel create notifications.
GuildBridgeEverything
GuildBridgeInvalid GuildBridgingMode = -1
)
func ParseGuildBridgingMode(str string) GuildBridgingMode {
str = strings.ToLower(str)
str = strings.ReplaceAll(str, "-", "")
str = strings.ReplaceAll(str, "_", "")
switch str {
case "nothing", "0":
return GuildBridgeNothing
case "ifportalexists", "1":
return GuildBridgeIfPortalExists
case "createonmessage", "2":
return GuildBridgeCreateOnMessage
case "everything", "3":
return GuildBridgeEverything
default:
return GuildBridgeInvalid
}
}
func (gbm GuildBridgingMode) String() string {
switch gbm {
case GuildBridgeNothing:
return "nothing"
case GuildBridgeIfPortalExists:
return "if-portal-exists"
case GuildBridgeCreateOnMessage:
return "create-on-message"
case GuildBridgeEverything:
return "everything"
default:
return ""
}
}
func (gbm GuildBridgingMode) Description() string {
switch gbm {
case GuildBridgeNothing:
return "never bridge messages"
case GuildBridgeIfPortalExists:
return "bridge messages in existing portals"
case GuildBridgeCreateOnMessage:
return "bridge all messages and create portals on first message"
case GuildBridgeEverything:
return "bridge all messages and create portals proactively"
default:
return ""
}
}
type GuildQuery struct { type GuildQuery struct {
db *Database db *Database
log log.Logger log log.Logger
} }
const ( const (
guildSelect = "SELECT dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, auto_bridge_channels FROM guild" guildSelect = "SELECT dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, bridging_mode FROM guild"
) )
func (gq *GuildQuery) New() *Guild { func (gq *GuildQuery) New() *Guild {
@@ -67,13 +132,13 @@ type Guild struct {
AvatarURL id.ContentURI AvatarURL id.ContentURI
AvatarSet bool AvatarSet bool
AutoBridgeChannels bool BridgingMode GuildBridgingMode
} }
func (g *Guild) Scan(row dbutil.Scannable) *Guild { func (g *Guild) Scan(row dbutil.Scannable) *Guild {
var mxid sql.NullString var mxid sql.NullString
var avatarURL string var avatarURL string
err := row.Scan(&g.ID, &mxid, &g.PlainName, &g.Name, &g.NameSet, &g.Avatar, &avatarURL, &g.AvatarSet, &g.AutoBridgeChannels) err := row.Scan(&g.ID, &mxid, &g.PlainName, &g.Name, &g.NameSet, &g.Avatar, &avatarURL, &g.AvatarSet, &g.BridgingMode)
if err != nil { if err != nil {
if !errors.Is(err, sql.ErrNoRows) { if !errors.Is(err, sql.ErrNoRows) {
g.log.Errorln("Database scan failed:", err) g.log.Errorln("Database scan failed:", err)
@@ -82,6 +147,9 @@ func (g *Guild) Scan(row dbutil.Scannable) *Guild {
return nil return nil
} }
if g.BridgingMode < GuildBridgeNothing || g.BridgingMode > GuildBridgeEverything {
panic(fmt.Errorf("invalid guild bridging mode %d in guild %s", g.BridgingMode, g.ID))
}
g.MXID = id.RoomID(mxid.String) g.MXID = id.RoomID(mxid.String)
g.AvatarURL, _ = id.ParseContentURI(avatarURL) g.AvatarURL, _ = id.ParseContentURI(avatarURL)
return g return g
@@ -96,10 +164,10 @@ func (g *Guild) mxidPtr() *id.RoomID {
func (g *Guild) Insert() { func (g *Guild) Insert() {
query := ` query := `
INSERT INTO guild (dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, auto_bridge_channels) INSERT INTO guild (dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, bridging_mode)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
` `
_, err := g.db.Exec(query, g.ID, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.AutoBridgeChannels) _, err := g.db.Exec(query, g.ID, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.BridgingMode)
if err != nil { if err != nil {
g.log.Warnfln("Failed to insert %s: %v", g.ID, err) g.log.Warnfln("Failed to insert %s: %v", g.ID, err)
panic(err) panic(err)
@@ -108,10 +176,10 @@ func (g *Guild) Insert() {
func (g *Guild) Update() { func (g *Guild) Update() {
query := ` query := `
UPDATE guild SET mxid=$1, plain_name=$2, name=$3, name_set=$4, avatar=$5, avatar_url=$6, avatar_set=$7, auto_bridge_channels=$8 UPDATE guild SET mxid=$1, plain_name=$2, name=$3, name_set=$4, avatar=$5, avatar_url=$6, avatar_set=$7, bridging_mode=$8
WHERE dcid=$9 WHERE dcid=$9
` `
_, err := g.db.Exec(query, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.AutoBridgeChannels, g.ID) _, err := g.db.Exec(query, g.mxidPtr(), g.PlainName, g.Name, g.NameSet, g.Avatar, g.AvatarURL.String(), g.AvatarSet, g.BridgingMode, g.ID)
if err != nil { if err != nil {
g.log.Warnfln("Failed to update %s: %v", g.ID, err) g.log.Warnfln("Failed to update %s: %v", g.ID, err)
panic(err) panic(err)

View File

@@ -1,4 +1,4 @@
-- v0 -> v13: Latest revision -- v0 -> v14: Latest revision
CREATE TABLE guild ( CREATE TABLE guild (
dcid TEXT PRIMARY KEY, dcid TEXT PRIMARY KEY,
@@ -10,7 +10,7 @@ CREATE TABLE guild (
avatar_url TEXT NOT NULL, avatar_url TEXT NOT NULL,
avatar_set BOOLEAN NOT NULL, avatar_set BOOLEAN NOT NULL,
auto_bridge_channels BOOLEAN NOT NULL bridging_mode INTEGER NOT NULL
); );
CREATE TABLE portal ( CREATE TABLE portal (

View File

@@ -0,0 +1,7 @@
-- v14: Add more modes of bridging guilds
ALTER TABLE guild ADD COLUMN bridging_mode INTEGER NOT NULL DEFAULT 0;
UPDATE guild SET bridging_mode=2 WHERE mxid<>'';
UPDATE guild SET bridging_mode=3 WHERE auto_bridge_channels=true;
ALTER TABLE guild DROP COLUMN auto_bridge_channels;
-- only: postgres
ALTER TABLE guild ALTER COLUMN bridging_mode DROP DEFAULT;

View File

@@ -312,6 +312,6 @@ func (guild *Guild) RemoveMXID() {
guild.MXID = "" guild.MXID = ""
guild.AvatarSet = false guild.AvatarSet = false
guild.NameSet = false guild.NameSet = false
guild.AutoBridgeChannels = false guild.BridgingMode = database.GuildBridgeNothing
guild.Update() guild.Update()
} }

View File

@@ -18,6 +18,7 @@ import (
"maunium.net/go/mautrix/bridge/bridgeconfig" "maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database"
"go.mau.fi/mautrix-discord/remoteauth" "go.mau.fi/mautrix-discord/remoteauth"
) )
@@ -429,11 +430,12 @@ func (p *ProvisioningAPI) reconnect(w http.ResponseWriter, r *http.Request) {
} }
type guildEntry struct { type guildEntry struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AvatarURL id.ContentURI `json:"avatar_url"` AvatarURL id.ContentURI `json:"avatar_url"`
MXID id.RoomID `json:"mxid"` MXID id.RoomID `json:"mxid"`
AutoBridge bool `json:"auto_bridge_channels"` AutoBridge bool `json:"auto_bridge_channels"`
BridgingMode string `json:"bridging_mode"`
} }
type respGuildsList struct { type respGuildsList struct {
@@ -451,11 +453,12 @@ func (p *ProvisioningAPI) guildsList(w http.ResponseWriter, r *http.Request) {
continue continue
} }
resp.Guilds = append(resp.Guilds, guildEntry{ resp.Guilds = append(resp.Guilds, guildEntry{
ID: guild.ID, ID: guild.ID,
Name: guild.PlainName, Name: guild.PlainName,
AvatarURL: guild.AvatarURL, AvatarURL: guild.AvatarURL,
MXID: guild.MXID, MXID: guild.MXID,
AutoBridge: guild.AutoBridgeChannels, AutoBridge: guild.BridgingMode == database.GuildBridgeEverything,
BridgingMode: guild.BridgingMode.String(),
}) })
} }
@@ -526,7 +529,7 @@ func (p *ProvisioningAPI) guildsUnbridge(w http.ResponseWriter, r *http.Request)
Error: "Guild not found", Error: "Guild not found",
ErrCode: mautrix.MNotFound.ErrCode, ErrCode: mautrix.MNotFound.ErrCode,
}) })
} else if !guild.AutoBridgeChannels && guild.MXID == "" { } else if guild.BridgingMode == database.GuildBridgeNothing && guild.MXID == "" {
jsonResponse(w, http.StatusNotFound, Error{ jsonResponse(w, http.StatusNotFound, Error{
Error: "That guild is not bridged", Error: "That guild is not bridged",
ErrCode: ErrCodeGuildNotBridged, ErrCode: ErrCodeGuildNotBridged,

27
user.go
View File

@@ -567,12 +567,15 @@ func (user *User) Disconnect() error {
return nil return nil
} }
func (user *User) bridgeMessage(guildID string) bool { func (user *User) getGuildBridgingMode(guildID string) database.GuildBridgingMode {
if guildID == "" { if guildID == "" {
return true return database.GuildBridgeEverything
} }
guild := user.bridge.GetGuildByID(guildID, false) guild := user.bridge.GetGuildByID(guildID, false)
return guild != nil && guild.MXID != "" if guild == nil {
return database.GuildBridgeNothing
}
return guild.BridgingMode
} }
func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) { func (user *User) readyHandler(_ *discordgo.Session, r *discordgo.Ready) {
@@ -769,7 +772,7 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp
if len(meta.Channels) > 0 { if len(meta.Channels) > 0 {
for _, ch := range meta.Channels { for _, ch := range meta.Channels {
portal := user.GetPortalByMeta(ch) portal := user.GetPortalByMeta(ch)
if guild.AutoBridgeChannels && portal.MXID == "" && user.channelIsBridgeable(ch) { if guild.BridgingMode >= database.GuildBridgeEverything && portal.MXID == "" && user.channelIsBridgeable(ch) {
err := portal.CreateMatrixRoom(user, ch) err := portal.CreateMatrixRoom(user, ch)
if err != nil { if err != nil {
user.log.Errorfln("Failed to create portal for guild channel %s/%s in initial sync: %v", guild.ID, ch.ID, err) user.log.Errorfln("Failed to create portal for guild channel %s/%s in initial sync: %v", guild.ID, ch.ID, err)
@@ -843,7 +846,7 @@ func (user *User) guildUpdateHandler(_ *discordgo.Session, g *discordgo.GuildUpd
} }
func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.ChannelCreate) { func (user *User) channelCreateHandler(_ *discordgo.Session, c *discordgo.ChannelCreate) {
if !user.bridgeMessage(c.GuildID) { if user.getGuildBridgingMode(c.GuildID) < database.GuildBridgeEverything {
user.log.Debugfln("Ignoring channel create event in unbridged guild %s/%s", c.GuildID, c.ID) user.log.Debugfln("Ignoring channel create event in unbridged guild %s/%s", c.GuildID, c.ID)
return return
} }
@@ -893,7 +896,8 @@ func (user *User) channelUpdateHandler(_ *discordgo.Session, c *discordgo.Channe
} }
func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildID string) { func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildID string) {
if !user.bridgeMessage(guildID) { if user.getGuildBridgingMode(guildID) <= database.GuildBridgeNothing {
// If guild bridging mode is nothing, don't even check if the portal exists
return return
} }
@@ -907,8 +911,7 @@ func (user *User) pushPortalMessage(msg interface{}, typeName, channelID, guildI
} }
portal = thread.Parent portal = thread.Parent
} }
// Double check because some messages don't have the guild ID specified. if mode := user.getGuildBridgingMode(portal.GuildID); mode <= database.GuildBridgeNothing || (portal.MXID == "" && mode <= database.GuildBridgeIfPortalExists) {
if !user.bridgeMessage(portal.GuildID) {
return return
} }
@@ -1150,7 +1153,9 @@ func (user *User) bridgeGuild(guildID string, everything bool) error {
} }
} }
} }
guild.AutoBridgeChannels = everything if everything {
guild.BridgingMode = database.GuildBridgeEverything
}
guild.Update() guild.Update()
user.log.Debugfln("Subscribing to guild %s after bridging", guild.ID) user.log.Debugfln("Subscribing to guild %s after bridging", guild.ID)
@@ -1177,10 +1182,10 @@ func (user *User) unbridgeGuild(guildID string) error {
} }
guild.roomCreateLock.Lock() guild.roomCreateLock.Lock()
defer guild.roomCreateLock.Unlock() defer guild.roomCreateLock.Unlock()
if !guild.AutoBridgeChannels && guild.MXID == "" { if guild.BridgingMode == database.GuildBridgeNothing && guild.MXID == "" {
return errors.New("that guild is not bridged") return errors.New("that guild is not bridged")
} }
guild.AutoBridgeChannels = false guild.BridgingMode = database.GuildBridgeNothing
guild.Update() guild.Update()
for _, portal := range user.bridge.GetAllPortalsInGuild(guild.ID) { for _, portal := range user.bridge.GetAllPortalsInGuild(guild.ID) {
portal.cleanup(false) portal.cleanup(false)