mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-09 20:00:12 +00:00
Harvey Tindall
54e4a51a7f
shared "newUser" method is now "NewUserPostVerification", and is shared between all routes which create a jellyfin account. The new "NewUserFromInvite", "NewUserFromAdmin" and "NewUserFromConfirmationKey" are smaller as a result. Discord, Telegram, and Matrix now implement the "ContactMethodLinker" and "ContactMethodUser" interfaces, meaning code is shared a lot between them in the NewUser methods, and the specifics are now in their own files. Ombi/Jellyseerr similarly implement a simpler interface "ThirdPartyService", which simply has ImportUser and AddContactMethod routes. Note these new interface methods are only used for user creation as of yet, but could likely be used in other places.
806 lines
26 KiB
Go
806 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|