diff --git a/config/bridge.go b/config/bridge.go index ad4e9bf..b6b01b2 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -28,9 +28,11 @@ import ( ) type BridgeConfig struct { - UsernameTemplate string `yaml:"username_template"` - DisplaynameTemplate string `yaml:"displayname_template"` - ChannelnameTemplate string `yaml:"channelname_template"` + UsernameTemplate string `yaml:"username_template"` + DisplaynameTemplate string `yaml:"displayname_template"` + ChannelNameTemplate string `yaml:"channel_name_template"` + GuildNameTemplate string `yaml:"guild_name_template"` + PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` DeliveryReceipts bool `yaml:"delivery_receipts"` MessageStatusEvents bool `yaml:"message_status_events"` @@ -62,7 +64,8 @@ type BridgeConfig struct { usernameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"` - channelnameTemplate *template.Template `yaml:"-"` + channelNameTemplate *template.Template `yaml:"-"` + guildNameTemplate *template.Template `yaml:"-"` } func (bc *BridgeConfig) GetResendBridgeInfo() bool { @@ -109,13 +112,15 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } else if !strings.Contains(bc.FormatUsername("1234567890"), "1234567890") { return fmt.Errorf("username template is missing user ID placeholder") } - bc.displaynameTemplate, err = template.New("displayname").Parse(bc.DisplaynameTemplate) if err != nil { return err } - - bc.channelnameTemplate, err = template.New("channelname").Parse(bc.ChannelnameTemplate) + bc.channelNameTemplate, err = template.New("channel_name").Parse(bc.ChannelNameTemplate) + if err != nil { + return err + } + bc.guildNameTemplate, err = template.New("guild_name").Parse(bc.GuildNameTemplate) if err != nil { return err } @@ -137,9 +142,9 @@ func (bc BridgeConfig) GetManagementRoomTexts() bridgeconfig.ManagementRoomTexts return bc.ManagementRoomText } -func (bc BridgeConfig) FormatUsername(userid string) string { +func (bc BridgeConfig) FormatUsername(userID string) string { var buffer strings.Builder - _ = bc.usernameTemplate.Execute(&buffer, userid) + _ = bc.usernameTemplate.Execute(&buffer, userID) return buffer.String() } @@ -149,45 +154,26 @@ func (bc BridgeConfig) FormatDisplayname(user *discordgo.User) string { return buffer.String() } -type wrappedChannel struct { - *discordgo.Channel - Guild string - Folder string +type ChannelNameParams struct { + Name string + ParentName string + GuildName string + NSFW bool + Type discordgo.ChannelType } -func (bc BridgeConfig) FormatChannelname(channel *discordgo.Channel, session *discordgo.Session) (string, error) { +func (bc BridgeConfig) FormatChannelName(params ChannelNameParams) string { var buffer strings.Builder - var guildName, folderName string - - if channel.Type != discordgo.ChannelTypeDM && channel.Type != discordgo.ChannelTypeGroupDM { - guild, err := session.Guild(channel.GuildID) - if err != nil { - return "", fmt.Errorf("find guild: %w", err) - } - guildName = guild.Name - - folder, err := session.Channel(channel.ParentID) - if err == nil { - folderName = folder.Name - } - } else { - // Group DM's can have a name, but DM's can't, so if we didn't get a - // name return a comma separated list of the formatted user names. - if channel.Name == "" { - recipients := make([]string, len(channel.Recipients)) - for idx, user := range channel.Recipients { - recipients[idx] = bc.FormatDisplayname(user) - } - - return strings.Join(recipients, ", "), nil - } - } - - _ = bc.channelnameTemplate.Execute(&buffer, wrappedChannel{ - Channel: channel, - Guild: guildName, - Folder: folderName, - }) - - return buffer.String(), nil + _ = bc.channelNameTemplate.Execute(&buffer, params) + return buffer.String() +} + +type GuildNameParams struct { + Name string +} + +func (bc BridgeConfig) FormatGuildName(params GuildNameParams) string { + var buffer strings.Builder + _ = bc.guildNameTemplate.Execute(&buffer, params) + return buffer.String() } diff --git a/config/upgrade.go b/config/upgrade.go index b43c9c3..1a4aa4f 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -27,7 +27,9 @@ func DoUpgrade(helper *up.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.Str, "bridge", "channel_name_template") + helper.Copy(up.Str, "bridge", "guild_name_template") + helper.Copy(up.Bool, "bridge", "private_chat_portal_meta") helper.Copy(up.Int, "bridge", "portal_message_buffer") helper.Copy(up.Bool, "bridge", "delivery_receipts") helper.Copy(up.Bool, "bridge", "message_status_events") diff --git a/database/guild.go b/database/guild.go index 2a09d69..a320a60 100644 --- a/database/guild.go +++ b/database/guild.go @@ -16,7 +16,7 @@ type GuildQuery struct { } const ( - guildSelect = "SELECT dcid, mxid, 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, auto_bridge_channels FROM guild" ) func (gq *GuildQuery) New() *Guild { @@ -60,6 +60,7 @@ type Guild struct { ID string MXID id.RoomID + PlainName string Name string NameSet bool Avatar string @@ -72,7 +73,7 @@ type Guild struct { func (g *Guild) Scan(row dbutil.Scannable) *Guild { var mxid sql.NullString var avatarURL string - err := row.Scan(&g.ID, &mxid, &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.AutoBridgeChannels) if err != nil { if !errors.Is(err, sql.ErrNoRows) { g.log.Errorln("Database scan failed:", err) @@ -92,12 +93,13 @@ func (g *Guild) mxidPtr() *id.RoomID { } return nil } + func (g *Guild) Insert() { query := ` - INSERT INTO guild (dcid, mxid, name, name_set, avatar, avatar_url, avatar_set, auto_bridge_channels) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + INSERT INTO guild (dcid, mxid, plain_name, name, name_set, avatar, avatar_url, avatar_set, auto_bridge_channels) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ` - _, err := g.db.Exec(query, g.ID, g.mxidPtr(), 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.AutoBridgeChannels) if err != nil { g.log.Warnfln("Failed to insert %s: %v", g.ID, err) panic(err) @@ -106,10 +108,10 @@ func (g *Guild) Insert() { func (g *Guild) Update() { query := ` - UPDATE guild SET mxid=$1, name=$2, name_set=$3, avatar=$4, avatar_url=$5, avatar_set=$6, auto_bridge_channels=$7 - WHERE dcid=$8 + 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 + WHERE dcid=$9 ` - _, err := g.db.Exec(query, g.mxidPtr(), 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.AutoBridgeChannels, g.ID) if err != nil { g.log.Warnfln("Failed to update %s: %v", g.ID, err) panic(err) diff --git a/database/portal.go b/database/portal.go index b5d5dce..d8ce90d 100644 --- a/database/portal.go +++ b/database/portal.go @@ -11,10 +11,14 @@ import ( "maunium.net/go/mautrix/util/dbutil" ) +// language=postgresql const ( - portalSelect = "SELECT dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, " + - " mxid, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, encrypted, in_space, first_event_id" + - " FROM portal" + portalSelect = ` + SELECT dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid, + plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, + encrypted, in_space, first_event_id + FROM portal + ` ) type PortalKey struct { @@ -101,6 +105,7 @@ type Portal struct { MXID id.RoomID + PlainName string Name string NameSet bool Topic string @@ -120,7 +125,7 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal { var avatarURL string err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &chanType, &otherUserID, &guildID, &parentID, - &mxid, &p.Name, &p.NameSet, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet, + &mxid, &p.PlainName, &p.Name, &p.NameSet, &p.Topic, &p.TopicSet, &p.Avatar, &avatarURL, &p.AvatarSet, &p.Encrypted, &p.InSpace, &firstEventID) if err != nil { @@ -146,13 +151,13 @@ func (p *Portal) Scan(row dbutil.Scannable) *Portal { func (p *Portal) Insert() { query := ` INSERT INTO portal (dcid, receiver, type, other_user_id, dc_guild_id, dc_parent_id, mxid, - name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, + plain_name, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, encrypted, in_space, first_event_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) ` _, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)), - p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, + p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.Encrypted, p.InSpace, p.FirstEventID.String()) if err != nil { @@ -163,14 +168,15 @@ func (p *Portal) Insert() { func (p *Portal) Update() { query := ` - UPDATE portal SET type=$1, other_user_id=$2, dc_guild_id=$3, dc_parent_id=$4, mxid=$5, - name=$6, name_set=$7, topic=$8, topic_set=$9, avatar=$10, avatar_url=$11, avatar_set=$12, - encrypted=$13, in_space=$14, first_event_id=$15 - WHERE dcid=$16 AND receiver=$17 + UPDATE portal + SET type=$1, other_user_id=$2, dc_guild_id=$3, dc_parent_id=$4, mxid=$5, + plain_name=$6, name=$7, name_set=$8, topic=$9, topic_set=$10, avatar=$11, avatar_url=$12, avatar_set=$13, + encrypted=$14, in_space=$15, first_event_id=$16 + WHERE dcid=$17 AND receiver=$18 ` _, err := p.db.Exec(query, p.Type, strPtr(p.OtherUserID), strPtr(p.GuildID), strPtr(p.ParentID), strPtr(string(p.MXID)), - p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, + p.PlainName, p.Name, p.NameSet, p.Topic, p.TopicSet, p.Avatar, p.AvatarURL.String(), p.AvatarSet, p.Encrypted, p.InSpace, p.FirstEventID.String(), p.Key.ChannelID, p.Key.Receiver) diff --git a/database/upgrades/00-latest-revision.sql b/database/upgrades/00-latest-revision.sql index ef17595..fbc584e 100644 --- a/database/upgrades/00-latest-revision.sql +++ b/database/upgrades/00-latest-revision.sql @@ -3,6 +3,7 @@ CREATE TABLE guild ( dcid TEXT PRIMARY KEY, mxid TEXT UNIQUE, + plain_name TEXT NOT NULL, name TEXT NOT NULL, name_set BOOLEAN NOT NULL, avatar TEXT NOT NULL, @@ -25,6 +26,7 @@ CREATE TABLE portal ( dc_parent_receiver TEXT NOT NULL DEFAULT '', mxid TEXT UNIQUE, + plain_name TEXT NOT NULL, name TEXT NOT NULL, name_set BOOLEAN NOT NULL, topic TEXT NOT NULL, diff --git a/database/upgrades/08-channel-plain-name.sql b/database/upgrades/08-channel-plain-name.sql new file mode 100644 index 0000000..22237b6 --- /dev/null +++ b/database/upgrades/08-channel-plain-name.sql @@ -0,0 +1,9 @@ +-- v8: Store plain name of channels and guilds +ALTER TABLE guild ADD COLUMN plain_name TEXT; +ALTER TABLE portal ADD COLUMN plain_name TEXT; +UPDATE guild SET plain_name=name; +UPDATE portal SET plain_name=name; +UPDATE portal SET plain_name='' WHERE type=1; +-- only: postgres for next 2 lines +ALTER TABLE guild ALTER COLUMN plain_name SET NOT NULL; +ALTER TABLE portal ALTER COLUMN plain_name SET NOT NULL; diff --git a/example-config.yaml b/example-config.yaml index 35510e0..674b788 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -67,15 +67,29 @@ bridge: # Localpart template of MXIDs for Discord users. # {{.}} is replaced with the internal ID of the Discord user. username_template: discord_{{.}} - # Displayname template for Discord users. - # Available fields: + # Displayname template for Discord users. This is also used as the room name in DMs if private_chat_portal_meta is enabled. + # Available variables: # .ID - Internal user ID # .Username - User's displayname on Discord # .Discriminator - The 4 numbers after the name on Discord # .Bot - Whether the user is a bot # .System - Whether the user is an official system user displayname_template: '{{.Username}}#{{.Discriminator}} {{if .Bot}} (bot){{end}}' - channelname_template: '{{if .Guild}}{{.Guild}} - {{end}}{{if .Folder}}{{.Folder}} - {{end}}{{.Name}} (D)' + # Displayname template for Discord channels (bridged as rooms, or spaces when type=4). + # Available variables: + # .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs. + # .ParentName - Parent channel name (used for categories). + # .GuildName - Guild name. + # .NSFW - Whether the channel is marked as NSFW. + # .Type - Channel type (see values at https://github.com/bwmarrin/discordgo/blob/v0.25.0/structs.go#L251-L267) + channel_name_template: '{{if or (eq .Type 3) (eq .Type 4)}}{{.Name}}{{else}}#{{.Name}}{{end}}' + # Displayname template for Discord guilds (bridged as spaces). + # Available variables: + # .Name - Guild name + guild_name_template: '{{.Name}}' + # Should the bridge explicitly set the avatar and room name for DM portal rooms? + # This is implicitly enabled in encrypted rooms. + private_chat_portal_meta: false portal_message_buffer: 128 @@ -133,7 +147,6 @@ bridge: allow: false # Default to encryption, force-enable encryption in all portals the bridge creates # This will cause the bridge bot to be in private chats for the encryption to work properly. - # It is recommended to also set private_chat_portal_meta to true when using this. default: false # Require encryption, drop any unencrypted messages. require: false diff --git a/guildportal.go b/guildportal.go index c5282b8..d3e5f86 100644 --- a/guildportal.go +++ b/guildportal.go @@ -28,6 +28,7 @@ import ( "github.com/bwmarrin/discordgo" + "go.mau.fi/mautrix-discord/config" "go.mau.fi/mautrix-discord/database" ) @@ -227,14 +228,7 @@ func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.G return meta } changed := false - // FIXME - //name, err := guild.bridge.Config.Bridge.FormatChannelname(meta, user.Session) - //if err != nil { - // guild.log.Warnfln("failed to format name, proceeding with generic name: %v", err) - // guild.Name = meta.Name - //} else { - //} - changed = guild.UpdateName(meta.Name) || changed + changed = guild.UpdateName(meta) || changed changed = guild.UpdateAvatar(meta.Icon) || changed if changed { guild.UpdateBridgeInfo() @@ -243,11 +237,15 @@ func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.G return meta } -func (guild *Guild) UpdateName(name string) bool { - if guild.Name == name && guild.NameSet { +func (guild *Guild) UpdateName(meta *discordgo.Guild) bool { + name := guild.bridge.Config.Bridge.FormatGuildName(config.GuildNameParams{ + Name: meta.Name, + }) + if guild.PlainName == meta.Name && guild.Name == name && guild.NameSet { return false } guild.Name = name + guild.PlainName = meta.Name guild.NameSet = false if guild.MXID != "" { _, err := guild.bridge.Bot.SetRoomName(guild.MXID, guild.Name) diff --git a/portal.go b/portal.go index c1fbaa9..d264614 100644 --- a/portal.go +++ b/portal.go @@ -23,6 +23,7 @@ import ( "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" + "go.mau.fi/mautrix-discord/config" "go.mau.fi/mautrix-discord/database" ) @@ -1509,11 +1510,29 @@ func (portal *Portal) HandleMatrixTyping(newTyping []id.UserID) { } } -func (portal *Portal) UpdateName(name string) bool { +func (portal *Portal) UpdateName(meta *discordgo.Channel) bool { + var parentName, guildName string + if portal.Parent != nil { + parentName = portal.Parent.PlainName + } + if portal.Guild != nil { + guildName = portal.Guild.PlainName + } + plainNameChanged := portal.PlainName != meta.Name + portal.PlainName = meta.Name + return portal.UpdateNameDirect(portal.bridge.Config.Bridge.FormatChannelName(config.ChannelNameParams{ + Name: meta.Name, + ParentName: parentName, + GuildName: guildName, + NSFW: meta.NSFW, + Type: meta.Type, + })) || plainNameChanged +} + +func (portal *Portal) UpdateNameDirect(name string) bool { if portal.Name == name && portal.NameSet { return false - } else if !portal.Encrypted && portal.IsPrivateChat() { - // TODO custom config option for always setting private chat portal meta? + } else if !portal.Encrypted && !portal.bridge.Config.Bridge.PrivateChatPortalMeta && portal.IsPrivateChat() { return false } portal.Name = name @@ -1708,25 +1727,18 @@ func (portal *Portal) UpdateInfo(source *User, meta *discordgo.Channel) *discord changed = true } - // FIXME - //name, err := portal.bridge.Config.Bridge.FormatChannelname(meta, source.Session) - //if err != nil { - // portal.log.Errorln("Failed to format channel name:", err) - // return - //} - switch portal.Type { case discordgo.ChannelTypeDM: if portal.OtherUserID != "" { puppet := portal.bridge.GetPuppetByID(portal.OtherUserID) changed = portal.UpdateAvatarFromPuppet(puppet) || changed - changed = portal.UpdateName(puppet.Name) || changed + changed = portal.UpdateNameDirect(puppet.Name) || changed } case discordgo.ChannelTypeGroupDM: changed = portal.UpdateGroupDMAvatar(meta.Icon) || changed fallthrough default: - changed = portal.UpdateName(meta.Name) || changed + changed = portal.UpdateName(meta) || changed } changed = portal.UpdateTopic(meta.Topic) || changed changed = portal.UpdateParent(meta.ParentID) || changed diff --git a/puppet.go b/puppet.go index f7bf048..2b8f82d 100644 --- a/puppet.go +++ b/puppet.go @@ -202,8 +202,9 @@ func (puppet *Puppet) UpdateName(info *discordgo.User) bool { puppet.log.Warnln("Failed to update displayname:", err) } else { go puppet.updatePortalMeta(func(portal *Portal) { - if portal.UpdateName(puppet.Name) { + if portal.UpdateNameDirect(puppet.Name) { portal.Update() + portal.UpdateBridgeInfo() } }) puppet.NameSet = true @@ -237,6 +238,7 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool { go puppet.updatePortalMeta(func(portal *Portal) { if portal.UpdateAvatarFromPuppet(puppet) { portal.Update() + portal.UpdateBridgeInfo() } }) puppet.AvatarSet = true