Эх сурвалжийг харах

Store role list in database and convert role mentions into a readable format

Tulir Asokan 3 жил өмнө
parent
commit
2611cbfa34

+ 5 - 0
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
 }
 

+ 114 - 0
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)
+	}
+}

+ 23 - 2
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
+);

+ 19 - 0
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
+);

+ 5 - 1
formatter_tag.go

@@ -263,7 +263,11 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
 		_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, 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, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name)
+			return
+		}
 	case *astDiscordChannelMention:
 		portal := r.portal.bridge.GetExistingPortalByID(database.PortalKey{
 			ChannelID: strconv.FormatInt(node.id, 10),

+ 2 - 2
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

+ 4 - 4
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=

+ 74 - 0
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)
 	}