package main import ( "fmt" "net/http" "strings" "time" dg "github.com/bwmarrin/discordgo" lm "github.com/hrfee/jfa-go/logmessages" "github.com/timshannon/badgerhold/v4" ) type DiscordDaemon struct { Stopped bool ShutdownChannel chan string bot *dg.Session username string tokens map[string]VerifToken // Map of pins to tokens. verifiedTokens map[string]DiscordUser // Map of token pins to discord users. Channel, InviteChannel struct{ ID, Name string } guildID string serverChannelName, serverName string users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start. roleID string app *appContext commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string) commandIDs []string commandDescriptions []*dg.ApplicationCommand } func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) { token := app.config.Section("discord").Key("token").String() if token == "" { return nil, fmt.Errorf("token was blank") } bot, err := dg.New("Bot " + token) if err != nil { return nil, err } dd := &DiscordDaemon{ Stopped: false, ShutdownChannel: make(chan string), bot: bot, tokens: map[string]VerifToken{}, verifiedTokens: map[string]DiscordUser{}, users: map[string]DiscordUser{}, app: app, roleID: app.config.Section("discord").Key("apply_role").String(), commandHandlers: map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string){}, commandIDs: []string{}, } dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart dd.commandHandlers["lang"] = dd.cmdLang dd.commandHandlers["pin"] = dd.cmdPIN dd.commandHandlers["inv"] = dd.cmdInvite for _, user := range app.storage.GetDiscord() { dd.users[user.ID] = user } return dd, nil } // SetTransport sets the http.Transport to use for requests. Can be used to set a proxy. func (d *DiscordDaemon) SetTransport(t *http.Transport) { d.bot.Client.Transport = t } // NewAuthToken generates an 8-character pin in the form "A1-2B-CD". func (d *DiscordDaemon) NewAuthToken() string { pin := genAuthToken() d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""} return pin } // NewAssignedAuthToken generates an 8-character pin in the form "A1-2B-CD", // and assigns it for access only with the given Jellyfin ID. func (d *DiscordDaemon) NewAssignedAuthToken(id string) string { pin := genAuthToken() d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id} return pin } func (d *DiscordDaemon) NewUnknownUser(channelID, userID, discrim, username string) DiscordUser { user := DiscordUser{ ChannelID: channelID, ID: userID, Username: username, Discriminator: discrim, } return user } func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) DiscordUser { if user, ok := d.users[userID]; ok { return user } return d.NewUnknownUser(channelID, userID, discrim, username) } func (d *DiscordDaemon) run() { d.bot.AddHandler(d.commandHandler) d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites if err := d.bot.Open(); err != nil { d.app.err.Printf(lm.FailedStartDaemon, lm.Discord, err) return } // Wait for everything to populate, it's slow sometimes. for d.bot.State == nil { continue } for d.bot.State.User == nil { continue } d.username = d.bot.State.User.Username for d.bot.State.Guilds == nil { continue } // Choose the last guild (server), for now we don't really support multiple anyway d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID guild, err := d.bot.Guild(d.guildID) if err != nil { d.app.err.Printf(lm.FailedGetDiscordGuild, err) } d.serverChannelName = guild.Name d.serverName = guild.Name if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" { d.Channel.Name = channel d.serverChannelName += "/" + channel } if d.app.config.Section("discord").Key("provide_invite").MustBool(false) { if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" { d.InviteChannel.Name = invChannel } } err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start")) defer d.deregisterCommands() defer d.bot.Close() go d.registerCommands() <-d.ShutdownChannel d.ShutdownChannel <- "Down" return } // ListRoles returns a list of available (excluding bot and @everyone) roles in a guild as a list of containing an array of the guild ID and its name. func (d *DiscordDaemon) ListRoles() (roles [][2]string, err error) { var r []*dg.Role r, err = d.bot.GuildRoles(d.guildID) if err != nil { d.app.err.Printf(lm.FailedGetDiscordRoles, err) return } for _, role := range r { if role.Name != d.username && role.Name != "@everyone" { roles = append(roles, [2]string{role.ID, role.Name}) } } // roles = make([][2]string, len(r)) // for i, role := range r { // roles[i] = [2]string{role.ID, role.Name} // } return } // ApplyRole applies the member role to the given user if set. func (d *DiscordDaemon) ApplyRole(userID string) error { if d.roleID == "" { return nil } return d.bot.GuildMemberRoleAdd(d.guildID, userID, d.roleID) } // RemoveRole removes the member role to the given user if set. func (d *DiscordDaemon) RemoveRole(userID string) error { if d.roleID == "" { return nil } return d.bot.GuildMemberRoleRemove(d.guildID, userID, d.roleID) } // SetRoleDisabled removes the role if "disabled", and applies if "!disabled". func (d *DiscordDaemon) SetRoleDisabled(userID string, disabled bool) (err error) { if disabled { err = d.RemoveRole(userID) } else { err = d.ApplyRole(userID) } return } // NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon. func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) { var inv *dg.Invite var err error if d.InviteChannel.Name == "" { d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty) return } if d.InviteChannel.ID == "" { channels, err := d.bot.GuildChannels(d.guildID) if err != nil { d.app.err.Printf(lm.FailedGetDiscordChannels, err) return } found := false for _, channel := range channels { // channel, err := d.bot.Channel(ch.ID) // if err != nil { // d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err) // return // } if channel.Name == d.InviteChannel.Name { d.InviteChannel.ID = channel.ID found = true break } } if !found { d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound) return } } // channel, err := d.bot.Channel(d.inviteChannelID) // if err != nil { // d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err) // return // } inv, err = d.bot.ChannelInviteCreate(d.InviteChannel.ID, dg.Invite{ // Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1], // Channel: channel, // Inviter: d.bot.State.User, MaxAge: ageSeconds, MaxUses: maxUses, Temporary: false, }) if err != nil { d.app.err.Printf(lm.FailedGenerateDiscordInvite, err) return } inviteURL = "https://discord.gg/" + inv.Code guild, err := d.bot.Guild(d.guildID) if err != nil { d.app.err.Printf(lm.FailedGetDiscordGuild, err) return } iconURL = guild.IconURL("256") return } // RenderDiscordUsername returns String of discord username, with support for new discriminator-less versions. func RenderDiscordUsername[DcUser *dg.User | DiscordUser](user DcUser) string { u, ok := interface{}(user).(*dg.User) var discriminator, username string if ok { discriminator = u.Discriminator username = u.Username } else { u2 := interface{}(user).(DiscordUser) discriminator = u2.Discriminator username = u2.Username } if discriminator == "0" { return "@" + username } return username + "#" + discriminator } // Returns the user(s) roughly corresponding to the username (if they are in the guild). // if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned. func (d *DiscordDaemon) GetUsers(username string) []*dg.Member { members, err := d.bot.GuildMembers( d.guildID, "", 1000, ) if err != nil { d.app.err.Printf(lm.FailedGetDiscordGuildMembers, err) return nil } hasDiscriminator := strings.Contains(username, "#") hasAt := strings.HasPrefix(username, "@") if hasAt { username = username[1:] } var users []*dg.Member for _, member := range members { if hasDiscriminator { if member.User.Username+"#"+member.User.Discriminator == username { return []*dg.Member{member} } } if hasAt { if member.User.Username == username && member.User.Discriminator == "0" { return []*dg.Member{member} } } if strings.Contains(member.User.Username, username) { users = append(users, member) } } return users } func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) { u, err := d.bot.User(ID) if err != nil { d.app.err.Printf(lm.FailedGetUser, ID, lm.Discord, err) return } user.ID = ID user.Username = u.Username user.Contact = true user.Discriminator = u.Discriminator channel, err := d.bot.UserChannelCreate(ID) if err != nil { d.app.err.Printf(lm.FailedCreateDiscordDMChannel, ID, err) return } user.ChannelID = channel.ID ok = true return } func (d *DiscordDaemon) Shutdown() { d.Stopped = true d.ShutdownChannel <- "Down" <-d.ShutdownChannel close(d.ShutdownChannel) } func (d *DiscordDaemon) registerCommands() { d.commandDescriptions = []*dg.ApplicationCommand{ { Name: d.app.config.Section("discord").Key("start_command").MustString("start"), Description: "Start the Discord linking process. The bot will send further instructions.", }, { Name: "lang", Description: "Set the language for the bot.", Options: []*dg.ApplicationCommandOption{ { Type: dg.ApplicationCommandOptionString, Name: "language", Description: "Language Name", Required: true, Choices: []*dg.ApplicationCommandOptionChoice{}, }, }, }, { Name: "pin", Description: "Send PIN for Discord verification.", Options: []*dg.ApplicationCommandOption{ { Type: dg.ApplicationCommandOptionString, Name: "pin", Description: "Verification PIN (e.g AB-CD-EF)", Required: true, }, }, }, { Name: "inv", Description: "Send an invite to a discord user (admin only).", Options: []*dg.ApplicationCommandOption{ { Type: dg.ApplicationCommandOptionUser, Name: "user", Description: "User to Invite.", Required: true, }, { Type: dg.ApplicationCommandOptionInteger, Name: "expiry", Description: "Time in minutes before expiration.", Required: false, }, /* Label should be automatically set to something like "Discord invite for @username" { Type: dg.ApplicationCommandOptionString, Name: "label", Description: "Label given to this invite (shown on the Admin page)", Required: false, }, */ { Type: dg.ApplicationCommandOptionString, Name: "user_label", Description: "Label given to users created with this invite.", Required: false, }, { Type: dg.ApplicationCommandOptionString, Name: "profile", Description: "Profile to apply to the created user.", Required: false, }, }, }, } d.commandDescriptions[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram)) i := 0 for code := range d.app.storage.lang.Telegram { d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Lang, d.app.storage.lang.Telegram[code].Meta.Name+":"+code) d.commandDescriptions[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{ Name: d.app.storage.lang.Telegram[code].Meta.Name, Value: code, } i++ } profiles := d.app.storage.GetProfiles() d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles)) for i, profile := range profiles { d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name) d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{ Name: profile.Name, Value: profile.Name, } } // d.deregisterCommands() d.commandIDs = make([]string, len(d.commandDescriptions)) // cCommands, err := d.bot.ApplicationCommandBulkOverwrite(d.bot.State.User.ID, d.guildID, commands) // if err != nil { // d.app.err.Printf("Discord: Cannot create commands: %v", err) // } for i, cmd := range d.commandDescriptions { command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd) if err != nil { d.app.err.Printf(lm.FailedRegisterDiscordCommand, cmd.Name, err) } else { d.app.debug.Printf(lm.RegisterDiscordCommand, cmd.Name) d.commandIDs[i] = command.ID } } } func (d *DiscordDaemon) deregisterCommands() { existingCommands, err := d.bot.ApplicationCommands(d.bot.State.User.ID, d.guildID) if err != nil { d.app.err.Printf(lm.FailedGetDiscordCommands, err) return } for _, cmd := range existingCommands { if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, d.guildID, cmd.ID); err != nil { d.app.err.Printf(lm.FailedDeregDiscordCommand, cmd.Name, err) } } } // UpdateCommands updates commands which have defined lists of options, to be used when changes occur. func (d *DiscordDaemon) UpdateCommands() { // Reload Profile List profiles := d.app.storage.GetProfiles() d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles)) for i, profile := range profiles { d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name) d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{ Name: profile.Name, Value: profile.Name, } } cmd, err := d.bot.ApplicationCommandEdit(d.bot.State.User.ID, d.guildID, d.commandIDs[3], d.commandDescriptions[3]) if err != nil { d.app.err.Printf(lm.FailedRegisterDiscordChoices, lm.Profile, err) } else { d.commandIDs[3] = cmd.ID } } func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) { if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok { if i.GuildID != "" && d.Channel.Name != "" { if d.Channel.ID == "" { channel, err := s.Channel(i.ChannelID) if err != nil { d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err) d.app.err.Println(lm.MonitorAllDiscordChannels) d.Channel.Name = "" } if channel.Name == d.Channel.Name { d.Channel.ID = channel.ID } } if d.Channel.ID != i.ChannelID { d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord) return } } if i.Interaction.Member.User.ID == s.State.User.ID { return } lang := d.app.storage.lang.chosenTelegramLang if user, ok := d.users[i.Interaction.Member.User.ID]; ok { if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok { lang = user.Lang } } h(s, i, lang) } } // cmd* methods handle slash-commands, msg* methods handle ! commands. func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang string) { channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID) if err != nil { d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err) return } user := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username) d.users[i.Interaction.Member.User.ID] = user content := d.app.storage.lang.Telegram[lang].Strings.get("discordStartMessage") + "\n" content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessageDiscord", tmpl{"command": "/lang"}) err = s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ // Type: dg.InteractionResponseChannelMessageWithSource, Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: content, Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err) return } } func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) { pin := i.ApplicationCommandData().Options[0].StringValue() user, ok := d.tokens[pin] if !ok || time.Now().After(user.Expiry) { err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ // Type: dg.InteractionResponseChannelMessageWithSource, Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"), Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err) } delete(d.tokens, pin) return } err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ // Type: dg.InteractionResponseChannelMessageWithSource, Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"), Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err) } dcUser := d.users[i.Interaction.Member.User.ID] dcUser.JellyfinID = user.JellyfinID d.verifiedTokens[pin] = dcUser delete(d.tokens, pin) } func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) { code := i.ApplicationCommandData().Options[0].StringValue() if _, ok := d.app.storage.lang.Telegram[code]; ok { var user DiscordUser for _, u := range d.app.storage.GetDiscord() { if u.ID == i.Interaction.Member.User.ID { u.Lang = code lang = code d.app.storage.SetDiscordKey(u.JellyfinID, u) user = u break } } d.users[i.Interaction.Member.User.ID] = user err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ // Type: dg.InteractionResponseChannelMessageWithSource, Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: d.app.storage.lang.Telegram[lang].Strings.template("languageSet", tmpl{"language": d.app.storage.lang.Telegram[lang].Meta.Name}), Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err) return } } } func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang string) { channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID) if err != nil { d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err) return } requester := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username) d.users[i.Interaction.Member.User.ID] = requester recipient := i.ApplicationCommandData().Options[0].UserValue(s) // d.app.debug.Println(invuser) //label := i.ApplicationCommandData().Options[2].StringValue() //profile := i.ApplicationCommandData().Options[3].StringValue() //mins, err := strconv.Atoi(i.ApplicationCommandData().Options[1].StringValue()) //if mins > 0 { // expmin = mins //} // Check whether requestor is linked to the admin account requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID) if !(ok && requesterEmail.Admin) { d.app.err.Printf(lm.FailedGenerateInvite, fmt.Sprintf(lm.NonAdminUser, requester.JellyfinID)) // FIXME: add response message return } var expiryMinutes int64 = 30 userLabel := "" profileName := "" for i, opt := range i.ApplicationCommandData().Options { if i == 0 { continue } switch opt.Name { case "expiry": expiryMinutes = opt.IntValue() case "user_label": userLabel = opt.StringValue() case "profile": profileName = opt.StringValue() } } currentTime := time.Now() validTill := currentTime.Add(time.Minute * time.Duration(expiryMinutes)) invite := Invite{ Code: GenerateInviteCode(), Created: currentTime, RemainingUses: 1, UserExpiry: false, ValidTill: validTill, UserLabel: userLabel, Profile: "Default", Label: fmt.Sprintf("%s: %s", lm.Discord, RenderDiscordUsername(recipient)), } if profileName != "" { if _, ok := d.app.storage.GetProfileKey(profileName); ok { invite.Profile = profileName } } if recipient != nil && d.app.config.Section("invite_emails").Key("enabled").MustBool(false) { invname, err := d.bot.GuildMember(d.guildID, recipient.ID) invite.SendTo = invname.User.Username msg, err := d.app.email.constructInvite(invite.Code, invite, d.app, false) if err != nil { invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err) d.app.err.Println(invite.SendTo) err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: d.app.storage.lang.Telegram[lang].Strings.get("sentInviteFailure"), Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err) } } else { var err error err = d.app.discord.SendDM(msg, recipient.ID) if err != nil { invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err) d.app.err.Println(invite.SendTo) err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: d.app.storage.lang.Telegram[lang].Strings.get("sentInviteFailure"), Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err) } } else { d.app.info.Printf(lm.SentInviteMessage, invite.Code, RenderDiscordUsername(recipient)) err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: d.app.storage.lang.Telegram[lang].Strings.get("sentInvite"), Flags: 64, // Ephemeral }, }) if err != nil { d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err) } } } } //if profile != "" { d.app.storage.SetInvitesKey(invite.Code, invite) } func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error { channels := make([]string, len(userID)) for i, id := range userID { channel, err := d.bot.UserChannelCreate(id) if err != nil { return err } channels[i] = channel.ID } return d.Send(message, channels...) } func (d *DiscordDaemon) Send(message *Message, channelID ...string) error { msg := "" var embeds []*dg.MessageEmbed if message.Markdown != "" { msg, embeds = StripAltText(message.Markdown, true) } else { msg = message.Text } for _, id := range channelID { var err error if len(embeds) != 0 { _, err = d.bot.ChannelMessageSendComplex( id, &dg.MessageSend{ Content: msg, Embed: embeds[0], }, ) if err != nil { return err } for i := 1; i < len(embeds); i++ { _, err := d.bot.ChannelMessageSendEmbed(id, embeds[i]) if err != nil { return err } } } else { _, err := d.bot.ChannelMessageSend( id, msg, ) if err != nil { return err } } } return nil } // UserVerified returns whether or not a token with the given PIN has been verified, and the user itself. func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) { u, ok := d.verifiedTokens[pin] // delete(d.verifiedTokens, pin) return &u, ok } // AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself. // Returns false if the given Jellyfin ID does not match the one in the user. func (d *DiscordDaemon) AssignedUserVerified(pin string, jfID string) (user DiscordUser, ok bool) { user, ok = d.verifiedTokens[pin] if ok && user.JellyfinID != jfID { ok = false } // delete(d.verifiedUsers, pin) return } // UserExists returns whether or not a user with the given ID exists. func (d *DiscordDaemon) UserExists(id string) bool { c, err := d.app.storage.db.Count(&DiscordUser{}, badgerhold.Where("ID").Eq(id)) return err != nil || c > 0 } // Exists returns whether or not the given user exists. func (d *DiscordDaemon) Exists(user ContactMethodUser) bool { return d.UserExists(user.MethodID().(string)) } // DeleteVerifiedToken removes the token with the given PIN. func (d *DiscordDaemon) DeleteVerifiedToken(PIN string) { delete(d.verifiedTokens, PIN) } func (d *DiscordDaemon) PIN(req newUserDTO) string { return req.DiscordPIN } func (d *DiscordDaemon) Name() string { return lm.Discord } func (d *DiscordDaemon) Required() bool { return d.app.config.Section("discord").Key("required").MustBool(false) } func (d *DiscordDaemon) UniqueRequired() bool { return d.app.config.Section("discord").Key("require_unique").MustBool(false) } func (d *DiscordDaemon) PostVerificationTasks(PIN string, u ContactMethodUser) error { err := d.ApplyRole(u.MethodID().(string)) if err != nil { return fmt.Errorf(lm.FailedSetDiscordMemberRole, err) } return err } func (d *DiscordUser) Name() string { return RenderDiscordUsername(*d) } func (d *DiscordUser) SetMethodID(id any) { d.ID = id.(string) } func (d *DiscordUser) MethodID() any { return d.ID } func (d *DiscordUser) SetJellyfin(id string) { d.JellyfinID = id } func (d *DiscordUser) Jellyfin() string { return d.JellyfinID } func (d *DiscordUser) SetAllowContactFromDTO(req newUserDTO) { d.Contact = req.DiscordContact } func (d *DiscordUser) SetAllowContact(contact bool) { d.Contact = contact } func (d *DiscordUser) AllowContact() bool { return d.Contact } func (d *DiscordUser) Store(st *Storage) { st.SetDiscordKey(d.Jellyfin(), *d) }