2025-11-24 12:41:40 -08:00
// mautrix-discord - A Matrix-Discord puppeting bridge.
2026-01-08 16:55:34 -08:00
// Copyright (C) 2026 Tulir Asokan
2025-11-24 12:41:40 -08:00
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2025-11-26 18:09:00 -08:00
package discordid
2025-11-24 12:41:40 -08:00
import (
2026-02-03 21:55:58 -08:00
"strconv"
2026-02-13 18:16:03 -08:00
"strings"
2026-02-03 21:55:58 -08:00
"time"
2025-11-24 12:41:40 -08:00
"github.com/bwmarrin/discordgo"
"maunium.net/go/mautrix/bridgev2/networkid"
)
2026-02-06 13:24:26 -08:00
// DeletedGuildUserID is a magic user ID that is used in place of an actual user
// ID once they have deleted their account. This only applies in non-private
// (i.e. guild) contexts, such as guild channel message authors and mentions.
//
// Note that this user ID can also appear in message content as part of user
// mention markup ("<@456226577798135808>").
const DeletedGuildUserID = "456226577798135808"
// DeletedGuildUser is the user returned from the Discord API as a stand-in for
// users who have since deleted their account. As the name suggests, this only
// applies to fetched entities within guilds.
var DeletedGuildUser = discordgo . User {
ID : DeletedGuildUserID ,
Username : "Deleted User" ,
Discriminator : "0000" ,
}
2026-02-03 21:55:58 -08:00
const DiscordEpochMillis = 1420070400000
// GenerateNonce creates a Discord-style snowflake nonce for message idempotency.
func GenerateNonce ( ) string {
snowflake := ( time . Now ( ) . UnixMilli ( ) - DiscordEpochMillis ) << 22
return strconv . FormatInt ( snowflake , 10 )
}
2026-02-01 21:03:17 -08:00
func MakeUserID ( userID string ) networkid . UserID {
return networkid . UserID ( userID )
}
func ParseUserID ( userID networkid . UserID ) string {
return string ( userID )
}
func MakeUserLoginID ( userID string ) networkid . UserLoginID {
return networkid . UserLoginID ( userID )
}
func ParseUserLoginID ( id networkid . UserLoginID ) string {
return string ( id )
}
// UserLoginIDToUserID converts a UserLoginID to a UserID. In Discord, both
// are the same underlying snowflake.
func UserLoginIDToUserID ( id networkid . UserLoginID ) networkid . UserID {
return networkid . UserID ( id )
}
2026-03-03 22:12:42 -08:00
// MakeChannelPortalKey creates a PortalKey from a Discord channel ID and the
// user login it was received from.
//
// If you can reach a DiscordClient, prefer calling the helper methods defined
// on it instead, as split portal configuration will be respected for you.
func MakeChannelPortalKey ( channelID string , userLoginID networkid . UserLoginID , wantReceiver bool ) ( key networkid . PortalKey ) {
key . ID = MakeChannelPortalIDWithID ( channelID )
2026-02-13 21:06:37 -08:00
if wantReceiver {
key . Receiver = userLoginID
}
return
}
func MakeChannelPortalKeyWithID ( channelID string ) ( key networkid . PortalKey ) {
key . ID = MakeChannelPortalIDWithID ( channelID )
return
}
func MakeChannelPortalIDWithID ( channelID string ) networkid . PortalID {
2026-02-01 21:03:17 -08:00
return networkid . PortalID ( channelID )
}
2026-02-13 21:06:37 -08:00
func ParseChannelPortalID ( portalID networkid . PortalID ) string {
2026-02-01 21:03:17 -08:00
return string ( portalID )
}
func MakeMessageID ( messageID string ) networkid . MessageID {
return networkid . MessageID ( messageID )
}
func ParseMessageID ( messageID networkid . MessageID ) string {
return string ( messageID )
}
func MakeEmojiID ( emojiName string ) networkid . EmojiID {
return networkid . EmojiID ( emojiName )
}
func ParseEmojiID ( emojiID networkid . EmojiID ) string {
return string ( emojiID )
}
func MakeAvatarID ( avatar string ) networkid . AvatarID {
return networkid . AvatarID ( avatar )
}
2026-03-19 23:33:14 -07:00
func MakePartID ( attachmentID string ) networkid . PartID {
return networkid . PartID ( attachmentID )
}
func ParsePartID ( attachmentID string ) string {
return string ( attachmentID )
}
2026-02-01 21:03:17 -08:00
// The string prepended to [networkid.PortalKey]s identifying spaces that
// bridge Discord guilds.
//
// Every Discord guild created before August 2017 contained a channel
// having _the same ID as the guild itself_. This channel also functioned as
// the "default channel" in that incoming members would view this channel by
// default. It was also impossible to delete.
//
// After this date, these "default channels" became deletable, and fresh guilds
// were no longer created with a channel that exactly corresponded to the guild
// ID.
//
// To accommodate Discord guilds created before this API change that have also
// never deleted the default channel, we need a way to distinguish between the
2026-02-13 21:06:37 -08:00
// guild and the default channel. Otherwise, we wouldn't be able to bridge both
// the channel portal as well as the guild space; their keys would conflict.
2026-02-01 21:03:17 -08:00
//
// "*" was chosen as the asterisk character is used to filter by guilds in
// the quick switcher (in Discord's first-party clients).
//
// For more information, see: https://discord.com/developers/docs/change-log#breaking-change-default-channels:~:text=New%20guilds%20will%20no%20longer.
const GuildPortalKeySigil = "*"
2026-02-13 21:06:37 -08:00
func MakeGuildPortalIDWithID ( guildID string ) networkid . PortalID {
2026-02-01 21:03:17 -08:00
return networkid . PortalID ( GuildPortalKeySigil + guildID )
}
2026-03-03 22:12:42 -08:00
func MakeGuildPortalKey ( guildID string , userLoginID networkid . UserLoginID , wantReceiver bool ) ( key networkid . PortalKey ) {
key . ID = MakeGuildPortalIDWithID ( guildID )
if wantReceiver {
key . Receiver = userLoginID
}
return
}
2026-02-13 18:16:03 -08:00
// ParseGuildPortalID converts a [network.PortalID] pointing to a guild space
// back into the guild's ID on Discord.
//
// If the portal ID does not point to a guild, then an empty string is returned.
func ParseGuildPortalID ( portalID networkid . PortalID ) string {
opaque := string ( portalID )
if strings . HasPrefix ( opaque , GuildPortalKeySigil ) {
guildID := opaque [ 1 : ]
return guildID
}
return ""
}