From 1f9af8df89fbf6279ced5f435569bd48d2bf113b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 23 May 2021 19:50:03 +0100 Subject: [PATCH] Discord: Add option to provide server invite When enabled, a temporary one-use invite is created and shown to the user on the account creation form. --- api.go | 27 ++++++++++++ config/config-base.json | 20 ++++++++- css/base.css | 5 +++ discord.go | 93 +++++++++++++++++++++++++++++++++++------ html/form-base.html | 2 + html/form.html | 1 + models.go | 5 +++ router.go | 3 ++ ts/form.ts | 22 ++++++++++ views.go | 2 + 10 files changed, 166 insertions(+), 14 deletions(-) diff --git a/api.go b/api.go index cb02894..4c1ccb2 100644 --- a/api.go +++ b/api.go @@ -2206,6 +2206,33 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) { respondBool(200, ok, gc) } +// @Summary Returns a 10-minute, one-use Discord server invite +// @Produce json +// @Success 200 {object} DiscordInviteDTO +// @Failure 400 {object} boolResponse +// @Failure 401 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Param invCode path string true "invite Code" +// @Router /invite/{invCode}/discord/invite [get] +// @tags Other +func (app *appContext) DiscordServerInvite(gc *gin.Context) { + if app.discord.inviteChannelName == "" { + respondBool(400, false, gc) + return + } + code := gc.Param("invCode") + if _, ok := app.storage.invites[code]; !ok { + respondBool(401, false, gc) + return + } + invURL, iconURL := app.discord.NewTempInvite(10*60, 1) + if invURL == "" { + respondBool(500, false, gc) + return + } + gc.JSON(200, DiscordInviteDTO{invURL, iconURL}) +} + // @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional). // @Produce json // @Success 200 {object} DiscordUsersDTO diff --git a/config/config-base.json b/config/config-base.json index 48fb173..36bad28 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -592,11 +592,29 @@ "name": "Channel to monitor", "required": false, "requires_restart": true, - "depens_true": "enabled", + "depends_true": "enabled", "type": "text", "value": "", "description": "Only listen to commands in specified channel. Leave blank to monitor all." }, + "provide_invite": { + "name": "Provide server invite", + "required": false, + "requires_restart": true, + "depends_true": "enabled", + "type": "bool", + "value": false, + "description": "Generate a one-time discord server invite for the account creation form. Required Bot permission \"Create instant invite\", you may need to re-add the bot to your server after." + }, + "invite_channel": { + "name": "Invite channel", + "required": false, + "requires_restart": true, + "depends_true": "provide_invite", + "type": "text", + "value": "", + "description": "Channel to invite new users to." + }, "language": { "name": "Language", "required": false, diff --git a/css/base.css b/css/base.css index 57d7f31..7d96684 100644 --- a/css/base.css +++ b/css/base.css @@ -498,6 +498,11 @@ td.img-circle { height: 32px; } +span.img-circle.lg { + width: 64px; + height: 64px; +} + span.shield.img-circle { padding: 0.2rem; } diff --git a/discord.go b/discord.go index 3e0b848..43c2ebf 100644 --- a/discord.go +++ b/discord.go @@ -8,16 +8,17 @@ import ( ) type DiscordDaemon struct { - Stopped bool - ShutdownChannel chan string - bot *dg.Session - username string - tokens []string - verifiedTokens map[string]DiscordUser // Map of tokens to discord users. - channelID, channelName string - serverChannelName string - users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start. - app *appContext + Stopped bool + ShutdownChannel chan string + bot *dg.Session + username string + tokens []string + verifiedTokens map[string]DiscordUser // Map of tokens to discord users. + channelID, channelName, inviteChannelID, inviteChannelName 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. + app *appContext } func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) { @@ -71,7 +72,7 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) func (d *DiscordDaemon) run() { d.bot.AddHandler(d.messageHandler) - d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers + d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites if err := d.bot.Open(); err != nil { d.app.err.Printf("Discord: Failed to start daemon: %v", err) return @@ -82,26 +83,92 @@ func (d *DiscordDaemon) run() { } d.username = d.bot.State.User.Username // Choose the last guild (server), for now we don't really support multiple anyway - guild, err := d.bot.Guild(d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID) + 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("Discord: Failed to get guild: %v", err) } d.serverChannelName = guild.Name + d.serverName = guild.Name if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" { d.channelName = 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.inviteChannelName = invChannel + } + } defer d.bot.Close() <-d.ShutdownChannel d.ShutdownChannel <- "Down" 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.inviteChannelName == "" { + d.app.err.Println("Discord: Cannot create invite without channel specified in settings.") + return + } + if d.inviteChannelID == "" { + channels, err := d.bot.GuildChannels(d.guildID) + if err != nil { + d.app.err.Printf("Discord: Couldn't get channel list: %v", err) + return + } + found := false + for _, channel := range channels { + // channel, err := d.bot.Channel(ch.ID) + // if err != nil { + // d.app.err.Printf("Discord: Couldn't get channel: %v", err) + // return + // } + if channel.Name == d.inviteChannelName { + d.inviteChannelID = channel.ID + found = true + break + } + } + if !found { + d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName) + return + } + } + // channel, err := d.bot.Channel(d.inviteChannelID) + // if err != nil { + // d.app.err.Printf("Discord: Couldn't get invite channel: %v", err) + // return + // } + inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, 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("Discord: Failed to create invite: %v", err) + return + } + inviteURL = "https://discord.gg/" + inv.Code + guild, err := d.bot.Guild(d.guildID) + if err != nil { + d.app.err.Printf("Discord: Failed to get guild: %v", err) + return + } + iconURL = guild.IconURL() + return +} + // 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.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID, + d.guildID, "", 1000, ) diff --git a/html/form-base.html b/html/form-base.html index e144f58..855253b 100644 --- a/html/form-base.html +++ b/html/form-base.html @@ -20,6 +20,8 @@ window.discordEnabled = {{ .discordEnabled }}; window.discordRequired = {{ .discordRequired }}; window.discordPIN = "{{ .discordPIN }}"; + window.discordInviteLink = {{ .discordInviteLink }}; + window.discordServerName = "{{ .discordServerName }}"; {{ end }} diff --git a/html/form.html b/html/form.html index c391707..6b785d0 100644 --- a/html/form.html +++ b/html/form.html @@ -43,6 +43,7 @@ {{ .strings.linkDiscord }}

{{ .discordSendPINMessage }}

{{ .discordPIN }}

+ {{ .strings.success }} diff --git a/models.go b/models.go index 890bb62..e14030d 100644 --- a/models.go +++ b/models.go @@ -276,3 +276,8 @@ type DiscordConnectUserDTO struct { JellyfinID string `json:"jf_id"` DiscordID string `json:"discord_id"` } + +type DiscordInviteDTO struct { + InviteURL string `json:"invite"` + IconURL string `json:"icon"` +} diff --git a/router.go b/router.go index 38e3c97..a717d49 100644 --- a/router.go +++ b/router.go @@ -123,6 +123,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) { } if discordEnabled { router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite) + if app.config.Section("discord").Key("provide_invite").MustBool(false) { + router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite) + } } } if *SWAGGER { diff --git a/ts/form.ts b/ts/form.ts index 8f76c89..94d3874 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -18,6 +18,8 @@ interface formWindow extends Window { discordRequired: boolean; discordPIN: string; discordStartCommand: string; + discordInviteLink: boolean; + discordServerName: string; userExpiryEnabled: boolean; userExpiryMonths: number; userExpiryDays: number; @@ -88,10 +90,30 @@ if (window.telegramEnabled) { }; } +interface DiscordInvite { + invite: string; + icon: string; +} + var discordVerified = false; if (window.discordEnabled) { window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired); const discordButton = document.getElementById("link-discord") as HTMLSpanElement; + if (window.discordInviteLink) { + _get("/invite/" + window.code + "/discord/invite", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + return; + } + const inv = req.response as DiscordInvite; + const link = document.getElementById("discord-invite") as HTMLAnchorElement; + link.classList.add("subheading", "link-center"); + link.href = inv.invite; + link.target = "_blank"; + link.innerHTML = `${window.discordServerName}`; + } + }); + } discordButton.onclick = () => { const waiting = document.getElementById("discord-waiting") as HTMLSpanElement; toggleLoader(waiting); diff --git a/views.go b/views.go index b083391..6d8e306 100644 --- a/views.go +++ b/views.go @@ -302,6 +302,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "command": `` + app.config.Section("discord").Key("start_command").MustString("!start") + ``, "server_channel": app.discord.serverChannelName, })) + data["discordServerName"] = app.discord.serverName + data["discordInviteLink"] = app.discord.inviteChannelName != "" } // if discordEnabled {