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)
}