From 2611cbfa342919824d61da3dcf7e8ddcb9958e99 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 8 Jul 2022 15:31:03 +0300 Subject: [PATCH] Store role list in database and convert role mentions into a readable format --- database/database.go | 5 + database/role.go | 114 +++++++++++++++++++++++ database/upgrades/00-latest-revision.sql | 25 ++++- database/upgrades/07-store-role-info.sql | 19 ++++ formatter_tag.go | 6 +- go.mod | 4 +- go.sum | 8 +- user.go | 74 +++++++++++++++ 8 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 database/role.go create mode 100644 database/upgrades/07-store-role-info.sql diff --git a/database/database.go b/database/database.go index cfd874d..34aca2d 100644 --- a/database/database.go +++ b/database/database.go @@ -22,6 +22,7 @@ type Database struct { Reaction *ReactionQuery Emoji *EmojiQuery Guild *GuildQuery + Role *RoleQuery } func New(baseDB *dbutil.Database) *Database { @@ -59,6 +60,10 @@ func New(baseDB *dbutil.Database) *Database { db: db, log: db.Log.Sub("Guild"), } + db.Role = &RoleQuery{ + db: db, + log: db.Log.Sub("Role"), + } return db } diff --git a/database/role.go b/database/role.go new file mode 100644 index 0000000..a50a6a0 --- /dev/null +++ b/database/role.go @@ -0,0 +1,114 @@ +package database + +import ( + "database/sql" + "errors" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/util/dbutil" + + "github.com/bwmarrin/discordgo" +) + +type RoleQuery struct { + db *Database + log log.Logger +} + +// language=postgresql +const ( + roleSelect = "SELECT dc_guild_id, dcid, name, icon, mentionable, managed, hoist, color, position, permissions FROM role" + roleUpsert = ` + INSERT INTO role (dc_guild_id, dcid, name, icon, mentionable, managed, hoist, color, position, permissions) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (dc_guild_id, dcid) DO UPDATE + SET name=excluded.name, icon=excluded.icon, mentionable=excluded.mentionable, managed=excluded.managed, + hoist=excluded.hoist, color=excluded.color, position=excluded.position, permissions=excluded.permissions + ` + roleDelete = "DELETE FROM role WHERE dc_guild_id=$1 AND dcid=$2" +) + +func (rq *RoleQuery) New() *Role { + return &Role{ + db: rq.db, + log: rq.log, + } +} + +func (rq *RoleQuery) GetByID(guildID, dcid string) *Role { + query := roleSelect + " WHERE dc_guild_id=$1 AND dcid=$2" + return rq.New().Scan(rq.db.QueryRow(query, guildID, dcid)) +} + +func (rq *RoleQuery) DeleteByID(guildID, dcid string) { + _, err := rq.db.Exec("DELETE FROM role WHERE dc_guild_id=$1 AND dcid=$2", guildID, dcid) + if err != nil { + rq.log.Warnfln("Failed to delete %s/%s: %v", guildID, dcid, err) + panic(err) + } +} + +func (rq *RoleQuery) GetAll(guildID string) []*Role { + rows, err := rq.db.Query(roleSelect+" WHERE dc_guild_id=$1", guildID) + if err != nil { + rq.log.Errorfln("Failed to query roles of %s: %v", guildID, err) + return nil + } + + var roles []*Role + for rows.Next() { + role := rq.New().Scan(rows) + if role != nil { + roles = append(roles, role) + } + } + + return roles +} + +type Role struct { + db *Database + log log.Logger + + GuildID string + + discordgo.Role +} + +func (r *Role) Scan(row dbutil.Scannable) *Role { + var icon sql.NullString + err := row.Scan(&r.GuildID, &r.ID, &r.Name, &icon, &r.Mentionable, &r.Managed, &r.Hoist, &r.Color, &r.Position, &r.Permissions) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + r.log.Errorln("Database scan failed:", err) + panic(err) + } + + return nil + } + r.Icon = icon.String + return r +} + +func (r *Role) Upsert(txn dbutil.Execable) { + if txn == nil { + txn = r.db + } + _, err := txn.Exec(roleUpsert, r.GuildID, r.ID, r.Name, strPtr(r.Icon), r.Mentionable, r.Managed, r.Hoist, r.Color, r.Position, r.Permissions) + if err != nil { + r.log.Warnfln("Failed to insert %s/%s: %v", r.GuildID, r.ID, err) + panic(err) + } +} + +func (r *Role) Delete(txn dbutil.Execable) { + if txn == nil { + txn = r.db + } + _, err := txn.Exec(roleDelete, r.GuildID, r.Icon) + if err != nil { + r.log.Warnfln("Failed to delete %s/%s: %v", r.GuildID, r.ID, err) + panic(err) + } +} diff --git a/database/upgrades/00-latest-revision.sql b/database/upgrades/00-latest-revision.sql index 31451bc..ef17595 100644 --- a/database/upgrades/00-latest-revision.sql +++ b/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v5: Latest revision +-- v0 -> v7: Latest revision CREATE TABLE guild ( dcid TEXT PRIMARY KEY, @@ -74,7 +74,9 @@ CREATE TABLE "user" ( discord_token TEXT, management_room TEXT, space_room TEXT, - dm_space_room TEXT + dm_space_room TEXT, + + read_state_version INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE user_portal ( @@ -126,3 +128,22 @@ CREATE TABLE emoji ( discord_name TEXT, matrix_url TEXT ); + +CREATE TABLE role ( + dc_guild_id TEXT, + dcid TEXT, + + name TEXT NOT NULL, + icon TEXT, + + mentionable BOOLEAN NOT NULL, + managed BOOLEAN NOT NULL, + hoist BOOLEAN NOT NULL, + + color INTEGER NOT NULL, + position INTEGER NOT NULL, + permissions BIGINT NOT NULL, + + PRIMARY KEY (dc_guild_id, dcid), + CONSTRAINT role_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild (dcid) ON DELETE CASCADE +); diff --git a/database/upgrades/07-store-role-info.sql b/database/upgrades/07-store-role-info.sql new file mode 100644 index 0000000..21f6a57 --- /dev/null +++ b/database/upgrades/07-store-role-info.sql @@ -0,0 +1,19 @@ +-- v7: Store role info +CREATE TABLE role ( + dc_guild_id TEXT, + dcid TEXT, + + name TEXT NOT NULL, + icon TEXT, + + mentionable BOOLEAN NOT NULL, + managed BOOLEAN NOT NULL, + hoist BOOLEAN NOT NULL, + + color INTEGER NOT NULL, + position INTEGER NOT NULL, + permissions BIGINT NOT NULL, + + PRIMARY KEY (dc_guild_id, dcid), + CONSTRAINT role_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild (dcid) ON DELETE CASCADE +); diff --git a/formatter_tag.go b/formatter_tag.go index 2b3b24b..bc26e72 100644 --- a/formatter_tag.go +++ b/formatter_tag.go @@ -263,7 +263,11 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [ _, _ = fmt.Fprintf(w, `%s`, puppet.MXID, puppet.Name) return case *astDiscordRoleMention: - // TODO + role := r.portal.bridge.DB.Role.GetByID(r.portal.GuildID, strconv.FormatInt(node.id, 10)) + if role != nil { + _, _ = fmt.Fprintf(w, `@%s`, role.Color, role.Name) + return + } case *astDiscordChannelMention: portal := r.portal.bridge.GetExistingPortalByID(database.PortalKey{ ChannelID: strconv.FormatInt(node.id, 10), diff --git a/go.mod b/go.mod index b419103..9a3928b 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/yuin/goldmark v1.4.12 maunium.net/go/maulogger/v2 v2.3.2 - maunium.net/go/mautrix v0.11.1-0.20220705131441-28320db1cc9c + maunium.net/go/mautrix v0.11.1-0.20220708121944-cda2329dd1df ) require ( @@ -26,4 +26,4 @@ require ( maunium.net/go/mauflag v1.0.0 // indirect ) -replace github.com/bwmarrin/discordgo => gitlab.com/beeper/discordgo v0.23.3-0.20220708095310-09da7ef6f6de +replace github.com/bwmarrin/discordgo => gitlab.com/beeper/discordgo v0.23.3-0.20220708122002-c27922e0ba67 diff --git a/go.sum b/go.sum index 2b95fa0..a1b24b4 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -gitlab.com/beeper/discordgo v0.23.3-0.20220708095310-09da7ef6f6de h1:XSKsxfGXfUf7KTyzM1NmzYkqetqiLivUULhXr7alZX4= -gitlab.com/beeper/discordgo v0.23.3-0.20220708095310-09da7ef6f6de/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +gitlab.com/beeper/discordgo v0.23.3-0.20220708122002-c27922e0ba67 h1:FSxw+90bXpsAJZfH5oz49LL33TAk4L/0U7eJW+He4ys= +gitlab.com/beeper/discordgo v0.23.3-0.20220708122002-c27922e0ba67/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -59,5 +59,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.11.1-0.20220705131441-28320db1cc9c h1:/vVqeoH1CLFEbBpmd6nczEmCxJ9dxImWX6MtkaJjd+8= -maunium.net/go/mautrix v0.11.1-0.20220705131441-28320db1cc9c/go.mod h1:Lj4pBam5P0zIvieIFHnGsuaj+xfFtI3y/sC8yGlyna8= +maunium.net/go/mautrix v0.11.1-0.20220708121944-cda2329dd1df h1:MvSfTply7Vn+02RukSqW02REGy2qYzDWm7tH+0i7Akc= +maunium.net/go/mautrix v0.11.1-0.20220708121944-cda2329dd1df/go.mod h1:Lj4pBam5P0zIvieIFHnGsuaj+xfFtI3y/sC8yGlyna8= diff --git a/user.go b/user.go index b5d8da7..643c277 100644 --- a/user.go +++ b/user.go @@ -459,6 +459,9 @@ func (user *User) Connect() error { user.Session.AddHandler(user.guildCreateHandler) user.Session.AddHandler(user.guildDeleteHandler) user.Session.AddHandler(user.guildUpdateHandler) + user.Session.AddHandler(user.guildRoleCreateHandler) + user.Session.AddHandler(user.guildRoleUpdateHandler) + user.Session.AddHandler(user.guildRoleDeleteHandler) user.Session.AddHandler(user.channelCreateHandler) user.Session.AddHandler(user.channelDeleteHandler) @@ -586,6 +589,74 @@ func (user *User) addGuildToSpace(guild *Guild) bool { return false } +func (user *User) discordRoleToDB(guildID string, role *discordgo.Role, dbRole *database.Role) (*database.Role, bool) { + var changed bool + if dbRole == nil { + dbRole = user.bridge.DB.Role.New() + dbRole.ID = role.ID + dbRole.GuildID = guildID + changed = true + } else { + changed = dbRole.Name != role.Name || + dbRole.Icon != role.Icon || + dbRole.Mentionable != role.Mentionable || + dbRole.Managed != role.Managed || + dbRole.Hoist != role.Hoist || + dbRole.Color != role.Color || + dbRole.Position != role.Position || + dbRole.Permissions != role.Permissions + } + dbRole.Role = *role + return dbRole, changed +} + +func (user *User) handleGuildRoles(guildID string, newRoles []*discordgo.Role) { + existingRoles := user.bridge.DB.Role.GetAll(guildID) + existingRoleMap := make(map[string]*database.Role, len(existingRoles)) + for _, role := range existingRoles { + existingRoleMap[role.ID] = role + } + txn, err := user.bridge.DB.Begin() + if err != nil { + user.log.Errorln("Failed to start transaction for guild role sync:", err) + panic(err) + return + } + for _, role := range newRoles { + dbRole, changed := user.discordRoleToDB(guildID, role, existingRoleMap[role.ID]) + delete(existingRoleMap, role.ID) + if changed { + dbRole.Upsert(txn) + } + } + for _, removeRole := range existingRoleMap { + removeRole.Delete(txn) + } + err = txn.Commit() + if err != nil { + user.log.Errorln("Failed to commit guild role sync:", err) + rollbackErr := txn.Rollback() + if rollbackErr != nil { + user.log.Errorln("Failed to rollback errored guild role sync:", rollbackErr) + } + panic(err) + } +} + +func (user *User) guildRoleCreateHandler(_ *discordgo.Session, r *discordgo.GuildRoleCreate) { + dbRole, _ := user.discordRoleToDB(r.GuildID, r.Role, nil) + dbRole.Upsert(nil) +} + +func (user *User) guildRoleUpdateHandler(_ *discordgo.Session, r *discordgo.GuildRoleUpdate) { + dbRole, _ := user.discordRoleToDB(r.GuildID, r.Role, nil) + dbRole.Upsert(nil) +} + +func (user *User) guildRoleDeleteHandler(_ *discordgo.Session, r *discordgo.GuildRoleDelete) { + user.bridge.DB.Role.DeleteByID(r.GuildID, r.RoleID) +} + func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSpace bool) { guild := user.bridge.GetGuildByID(meta.ID, true) guild.UpdateInfo(user, meta) @@ -602,6 +673,9 @@ func (user *User) handleGuild(meta *discordgo.Guild, timestamp time.Time, isInSp } } } + if len(meta.Roles) > 0 { + user.handleGuildRoles(meta.ID, meta.Roles) + } if !isInSpace { isInSpace = user.addGuildToSpace(guild) }