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.
This commit is contained in:
Harvey Tindall 2021-05-23 19:50:03 +01:00
parent 0676b6c41f
commit 1f9af8df89
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
10 changed files with 166 additions and 14 deletions

27
api.go
View File

@ -2206,6 +2206,33 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
respondBool(200, ok, gc) 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). // @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
// @Produce json // @Produce json
// @Success 200 {object} DiscordUsersDTO // @Success 200 {object} DiscordUsersDTO

View File

@ -592,11 +592,29 @@
"name": "Channel to monitor", "name": "Channel to monitor",
"required": false, "required": false,
"requires_restart": true, "requires_restart": true,
"depens_true": "enabled", "depends_true": "enabled",
"type": "text", "type": "text",
"value": "", "value": "",
"description": "Only listen to commands in specified channel. Leave blank to monitor all." "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": { "language": {
"name": "Language", "name": "Language",
"required": false, "required": false,

View File

@ -498,6 +498,11 @@ td.img-circle {
height: 32px; height: 32px;
} }
span.img-circle.lg {
width: 64px;
height: 64px;
}
span.shield.img-circle { span.shield.img-circle {
padding: 0.2rem; padding: 0.2rem;
} }

View File

@ -8,16 +8,17 @@ import (
) )
type DiscordDaemon struct { type DiscordDaemon struct {
Stopped bool Stopped bool
ShutdownChannel chan string ShutdownChannel chan string
bot *dg.Session bot *dg.Session
username string username string
tokens []string tokens []string
verifiedTokens map[string]DiscordUser // Map of tokens to discord users. verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
channelID, channelName string channelID, channelName, inviteChannelID, inviteChannelName string
serverChannelName string guildID string
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start. serverChannelName, serverName string
app *appContext 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) { func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
@ -71,7 +72,7 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string)
func (d *DiscordDaemon) run() { func (d *DiscordDaemon) run() {
d.bot.AddHandler(d.messageHandler) 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 { if err := d.bot.Open(); err != nil {
d.app.err.Printf("Discord: Failed to start daemon: %v", err) d.app.err.Printf("Discord: Failed to start daemon: %v", err)
return return
@ -82,26 +83,92 @@ func (d *DiscordDaemon) run() {
} }
d.username = d.bot.State.User.Username d.username = d.bot.State.User.Username
// Choose the last guild (server), for now we don't really support multiple anyway // 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 { if err != nil {
d.app.err.Printf("Discord: Failed to get guild: %v", err) d.app.err.Printf("Discord: Failed to get guild: %v", err)
} }
d.serverChannelName = guild.Name d.serverChannelName = guild.Name
d.serverName = guild.Name
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" { if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
d.channelName = channel d.channelName = channel
d.serverChannelName += "/" + 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() defer d.bot.Close()
<-d.ShutdownChannel <-d.ShutdownChannel
d.ShutdownChannel <- "Down" d.ShutdownChannel <- "Down"
return 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). // 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. // 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 { func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
members, err := d.bot.GuildMembers( members, err := d.bot.GuildMembers(
d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID, d.guildID,
"", "",
1000, 1000,
) )

View File

@ -20,6 +20,8 @@
window.discordEnabled = {{ .discordEnabled }}; window.discordEnabled = {{ .discordEnabled }};
window.discordRequired = {{ .discordRequired }}; window.discordRequired = {{ .discordRequired }};
window.discordPIN = "{{ .discordPIN }}"; window.discordPIN = "{{ .discordPIN }}";
window.discordInviteLink = {{ .discordInviteLink }};
window.discordServerName = "{{ .discordServerName }}";
</script> </script>
<script src="js/form.js" type="module"></script> <script src="js/form.js" type="module"></script>
{{ end }} {{ end }}

View File

@ -43,6 +43,7 @@
<span class="heading mb-1">{{ .strings.linkDiscord }}</span> <span class="heading mb-1">{{ .strings.linkDiscord }}</span>
<p class="content mb-1"> {{ .discordSendPINMessage }}</p> <p class="content mb-1"> {{ .discordSendPINMessage }}</p>
<h1 class="ac">{{ .discordPIN }}</h1> <h1 class="ac">{{ .discordPIN }}</h1>
<a id="discord-invite"></a>
<span class="button ~info !normal full-width center mt-1" id="discord-waiting">{{ .strings.success }}</span> <span class="button ~info !normal full-width center mt-1" id="discord-waiting">{{ .strings.success }}</span>
</div> </div>
</div> </div>

View File

@ -276,3 +276,8 @@ type DiscordConnectUserDTO struct {
JellyfinID string `json:"jf_id"` JellyfinID string `json:"jf_id"`
DiscordID string `json:"discord_id"` DiscordID string `json:"discord_id"`
} }
type DiscordInviteDTO struct {
InviteURL string `json:"invite"`
IconURL string `json:"icon"`
}

View File

@ -123,6 +123,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
} }
if discordEnabled { if discordEnabled {
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite) 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 { if *SWAGGER {

View File

@ -18,6 +18,8 @@ interface formWindow extends Window {
discordRequired: boolean; discordRequired: boolean;
discordPIN: string; discordPIN: string;
discordStartCommand: string; discordStartCommand: string;
discordInviteLink: boolean;
discordServerName: string;
userExpiryEnabled: boolean; userExpiryEnabled: boolean;
userExpiryMonths: number; userExpiryMonths: number;
userExpiryDays: number; userExpiryDays: number;
@ -88,10 +90,30 @@ if (window.telegramEnabled) {
}; };
} }
interface DiscordInvite {
invite: string;
icon: string;
}
var discordVerified = false; var discordVerified = false;
if (window.discordEnabled) { if (window.discordEnabled) {
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired); window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
const discordButton = document.getElementById("link-discord") as HTMLSpanElement; 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 = `<span class="img-circle lg mr-1"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`;
}
});
}
discordButton.onclick = () => { discordButton.onclick = () => {
const waiting = document.getElementById("discord-waiting") as HTMLSpanElement; const waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
toggleLoader(waiting); toggleLoader(waiting);

View File

@ -302,6 +302,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"command": `<code class="code">` + app.config.Section("discord").Key("start_command").MustString("!start") + `</code>`, "command": `<code class="code">` + app.config.Section("discord").Key("start_command").MustString("!start") + `</code>`,
"server_channel": app.discord.serverChannelName, "server_channel": app.discord.serverChannelName,
})) }))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
} }
// if discordEnabled { // if discordEnabled {