1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2025-01-06 08:20:11 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
1f9af8df89
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.
2021-05-23 19:50:03 +01:00
0676b6c41f
Discord: Display channel on account creation form 2021-05-23 17:31:20 +01:00
ce8cdced4d
Discord: Fix GetUsers, add invite messages
The "Send to" box on the invite tab now accepts username#discriminator,
and a search icon has been added which opens a search window similar to
the one on the accounts tab. DiscordDaemon.GetUsers was also very broken
and wouldn't work with full username#discriminator, that's been fixed.
2021-05-23 16:16:31 +01:00
b8e3fc636c
Accounts: Fix cog on telegram when no discord linked
Also, disable telegram & discord if an auth/initialization error occurs.
2021-05-23 14:48:36 +01:00
519a5615cc
Accounts: Fix email check on dropdown 2021-05-23 14:35:01 +01:00
168b217553
Discord: fix user links 2021-05-23 14:32:35 +01:00
7d698d63e3
Discord: split discord search into own module
Will also be used for "Send to" on the invite page.
2021-05-23 14:22:18 +01:00
19 changed files with 413 additions and 163 deletions

83
api.go
View File

@ -826,18 +826,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.UserMinutes = req.UserMinutes invite.UserMinutes = req.UserMinutes
} }
invite.ValidTill = validTill invite.ValidTill = validTill
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
app.debug.Printf("%s: Sending invite email", inviteCode) addressValid := false
invite.Email = req.Email discord := ""
msg, err := app.email.constructInvite(inviteCode, invite, app, false) app.debug.Printf("%s: Sending invite message", inviteCode)
if err != nil { if discordEnabled && !strings.Contains(req.SendTo, "@") {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) users := app.discord.GetUsers(req.SendTo)
app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err) if len(users) == 0 {
} else if err := app.email.send(msg, req.Email); err != nil { invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) } else if len(users) > 1 {
app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err) invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
} else { } else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email) invite.SendTo = req.SendTo
addressValid = true
discord = users[0].User.ID
}
} else if emailEnabled {
addressValid = true
invite.SendTo = req.SendTo
}
if addressValid {
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
} else {
var err error
if discord != "" {
err = app.discord.SendDM(msg, discord)
} else {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
} else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
}
}
} }
} }
if req.Profile != "" { if req.Profile != "" {
@ -901,8 +927,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
if inv.RemainingUses != 0 { if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses invite.RemainingUses = inv.RemainingUses
} }
if inv.Email != "" { if inv.SendTo != "" {
invite.Email = inv.Email invite.SendTo = inv.SendTo
} }
if len(inv.Notify) != 0 { if len(inv.Notify) != 0 {
var address string var address string
@ -1203,7 +1229,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
} }
if email, ok := app.storage.emails[jfUser.ID]; ok { if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.Addr user.Email = email.Addr
user.NotifyThroughEmail = user.Email != "" user.NotifyThroughEmail = email.Contact
} }
expiry, ok := app.storage.users[jfUser.ID] expiry, ok := app.storage.users[jfUser.ID]
if ok { if ok {
@ -2180,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,13 +83,21 @@ 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
}
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
@ -96,11 +105,70 @@ func (d *DiscordDaemon) run() {
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,
) )
@ -111,13 +179,13 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
hasDiscriminator := strings.Contains(username, "#") hasDiscriminator := strings.Contains(username, "#")
var users []*dg.Member var users []*dg.Member
for _, member := range members { for _, member := range members {
if !hasDiscriminator { if hasDiscriminator {
userSplit := strings.Split(member.User.Username, "#") if member.User.Username+"#"+member.User.Discriminator == username {
if strings.Contains(userSplit[0], username) { return []*dg.Member{member}
users = append(users, member)
} }
} else if strings.Contains(member.User.Username, username) { }
return nil if strings.Contains(member.User.Username, username) {
users = append(users, member)
} }
} }
return users return users
@ -283,6 +351,18 @@ func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []s
d.tokens = d.tokens[:len(d.tokens)-1] d.tokens = d.tokens[:len(d.tokens)-1]
} }
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 { func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
msg := "" msg := ""
var embeds []*dg.MessageEmbed var embeds []*dg.MessageEmbed

View File

@ -331,8 +331,8 @@
{{ if .discord_enabled }} {{ if .discord_enabled }}
<div id="modal-discord" class="modal"> <div id="modal-discord" class="modal">
<div class="modal-content card"> <div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkDiscord }}<span class="modal-close">&times;</span></span> <span class="heading mb-1"><span id="discord-header"></span><span class="modal-close">&times;</span></span>
<p class="content mb-1">{{ .strings.searchDiscordUser }}</p> <p class="content mb-1" id="discord-description"></p>
<div class="row"> <div class="row">
<input type="search" class="col sm field ~neutral !normal input" id="discord-search" placeholder="user#1234"> <input type="search" class="col sm field ~neutral !normal input" id="discord-search" placeholder="user#1234">
</div> </div>
@ -501,7 +501,14 @@
<div id="create-send-to-container"> <div id="create-send-to-container">
<label class="label supra">{{ .strings.inviteSendToEmail }}</label> <label class="label supra">{{ .strings.inviteSendToEmail }}</label>
<div class="flex-expand mb-1 mt-half"> <div class="flex-expand mb-1 mt-half">
{{ if .discord_enabled }}
<input type="text" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com | user#1234">
<span id="create-send-to-search" class="button ~neutral !normal mr-1">
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
</span>
{{ else }}
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com"> <input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
{{ end }}
<label for="create-send-to-enabled" class="button ~neutral !normal"> <label for="create-send-to-enabled" class="button ~neutral !normal">
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled"> <input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
</label> </label>

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

@ -21,6 +21,7 @@
"apply": "Apply", "apply": "Apply",
"delete": "Delete", "delete": "Delete",
"add": "Add", "add": "Add",
"select": "Select",
"name": "Name", "name": "Name",
"date": "Date", "date": "Date",
"enabled": "Enabled", "enabled": "Enabled",
@ -96,7 +97,8 @@
"notifyInviteExpiry": "On expiry", "notifyInviteExpiry": "On expiry",
"notifyUserCreation": "On user creation", "notifyUserCreation": "On user creation",
"sendPIN": "Ask the user to send the PIN below to the bot.", "sendPIN": "Ask the user to send the PIN below to the bot.",
"searchDiscordUser": "Start typing the Discord username to link it." "searchDiscordUser": "Start typing the Discord username to find the user.",
"findDiscordUser": "Find Discord user"
}, },
"notifications": { "notifications": {
"changedEmailAddress": "Changed email address of {n}.", "changedEmailAddress": "Changed email address of {n}.",

View File

@ -570,6 +570,7 @@ func start(asDaemon, firstCall bool) {
app.telegram, err = newTelegramDaemon(app) app.telegram, err = newTelegramDaemon(app)
if err != nil { if err != nil {
app.err.Printf("Failed to authenticate with Telegram: %v", err) app.err.Printf("Failed to authenticate with Telegram: %v", err)
telegramEnabled = false
} else { } else {
go app.telegram.run() go app.telegram.run()
defer app.telegram.Shutdown() defer app.telegram.Shutdown()
@ -579,6 +580,7 @@ func start(asDaemon, firstCall bool) {
app.discord, err = newDiscordDaemon(app) app.discord, err = newDiscordDaemon(app)
if err != nil { if err != nil {
app.err.Printf("Failed to authenticate with Discord: %v", err) app.err.Printf("Failed to authenticate with Discord: %v", err)
discordEnabled = false
} else { } else {
go app.discord.run() go app.discord.run()
defer app.discord.Shutdown() defer app.discord.Shutdown()

View File

@ -50,7 +50,7 @@ type generateInviteDTO struct {
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
@ -100,7 +100,7 @@ type inviteDTO struct {
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable) RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable) SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable)
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
@ -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

@ -95,7 +95,7 @@ type Invite struct {
UserDays int `json:"user-days,omitempty"` UserDays int `json:"user-days,omitempty"`
UserHours int `json:"user-hours,omitempty"` UserHours int `json:"user-hours,omitempty"`
UserMinutes int `json:"user-minutes,omitempty"` UserMinutes int `json:"user-minutes,omitempty"`
Email string `json:"email"` SendTo string `json:"email"`
// Used to be stored as formatted time, now as Unix. // Used to be stored as formatted time, now as Unix.
UsedBy [][]string `json:"used-by"` UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"` Notify map[string]map[string]bool `json:"notify"`
@ -105,23 +105,24 @@ type Invite struct {
} }
type Lang struct { type Lang struct {
AdminPath string AdminPath string
chosenAdminLang string chosenAdminLang string
Admin adminLangs Admin adminLangs
AdminJSON map[string]string AdminJSON map[string]string
FormPath string FormPath string
chosenFormLang string chosenFormLang string
Form formLangs Form formLangs
PasswordResetPath string PasswordResetPath string
chosenPWRLang string chosenPWRLang string
PasswordReset pwrLangs PasswordReset pwrLangs
EmailPath string EmailPath string
chosenEmailLang string chosenEmailLang string
Email emailLangs Email emailLangs
CommonPath string CommonPath string
Common commonLangs Common commonLangs
SetupPath string SetupPath string
Setup setupLangs Setup setupLangs
// Telegram translations are also used for Discord bots (and likely future ones).
chosenTelegramLang string chosenTelegramLang string
TelegramPath string TelegramPath string
Telegram telegramLangs Telegram telegramLangs

View File

@ -83,6 +83,7 @@ func (t *TelegramDaemon) run() {
updates, err := t.bot.GetUpdatesChan(u) updates, err := t.bot.GetUpdatesChan(u)
if err != nil { if err != nil {
t.app.err.Printf("Failed to start Telegram daemon: %v", err) t.app.err.Printf("Failed to start Telegram daemon: %v", err)
telegramEnabled = false
return return
} }
for { for {

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

@ -2,6 +2,7 @@ import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateStri
import { templateEmail } from "../modules/settings.js"; import { templateEmail } from "../modules/settings.js";
import { Marked } from "@ts-stack/markdown"; import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js"; import { stripMarkdown } from "../modules/stripmd.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
interface User { interface User {
id: string; id: string;
@ -23,12 +24,8 @@ interface getPinResponse {
token: string; token: string;
username: string; username: string;
} }
interface DiscordUser { var addDiscord: (passData: string) => void;
name: string;
avatar_url: string;
id: string;
}
class user implements User { class user implements User {
private _row: HTMLTableRowElement; private _row: HTMLTableRowElement;
@ -51,7 +48,7 @@ class user implements User {
private _expiryUnix: number; private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement; private _lastActive: HTMLTableDataCellElement;
private _lastActiveUnix: number; private _lastActiveUnix: number;
id: string; id = "";
private _selected: boolean; private _selected: boolean;
get selected(): boolean { return this._selected; } get selected(): boolean { return this._selected; }
@ -106,7 +103,7 @@ class user implements User {
email.checked = s; email.checked = s;
} }
} }
if (window.discordEnabled && this._discordUsername != "") { if (window.discordEnabled && this._discordUsername) {
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement; const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
email.checked = s; email.checked = s;
} }
@ -121,11 +118,11 @@ class user implements User {
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram; (this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else { } else {
let innerHTML = ` let innerHTML = `
<a href="https://t.me/${u}" target="_blank">@${u}</a> <div class="table-inline">
<a href="https://t.me/${u}" target="_blank">@${u}</a>
`; `;
if (!window.discordEnabled || this._discordUsername == "") { if (!window.discordEnabled || !this._discordUsername) {
innerHTML += ` innerHTML += `
<div class="table-inline">
<i class="icon ri-settings-2-line ml-half dropdown-button"></i> <i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual"> <div class="dropdown manual">
<div class="dropdown-display lg"> <div class="dropdown-display lg">
@ -142,11 +139,11 @@ class user implements User {
</div> </div>
</div> </div>
</div> </div>
</div>
`; `;
} }
innerHTML += "</div>";
this._telegram.innerHTML = innerHTML; this._telegram.innerHTML = innerHTML;
if (!window.discordEnabled || this._discordUsername == "") { if (!window.discordEnabled || !this._discordUsername) {
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username. // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
const button = this._telegram.querySelector("i"); const button = this._telegram.querySelector("i");
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement; const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
@ -177,7 +174,7 @@ class user implements User {
if (telegram) { if (telegram) {
telegram.checked = s; telegram.checked = s;
} }
if (window.discordEnabled && this._discordUsername != "") { if (window.discordEnabled && this._discordUsername) {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement; const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s; telegram.checked = s;
} }
@ -192,11 +189,11 @@ class user implements User {
id: this.id, id: this.id,
email: email.checked email: email.checked
} }
if (window.telegramEnabled && this._telegramUsername != "") { if (window.telegramEnabled && this._telegramUsername) {
const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement; const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
send["telegram"] = telegram.checked; send["telegram"] = telegram.checked;
} }
if (window.discordEnabled && this._discordUsername != "") { if (window.discordEnabled && this._discordUsername) {
const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement; const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
send["discord"] = discord.checked; send["discord"] = discord.checked;
} }
@ -226,7 +223,7 @@ class user implements User {
this._discordUsername = u; this._discordUsername = u;
if (u == "") { if (u == "") {
this._discord.innerHTML = `<span class="chip btn !low">Add</span>`; this._discord.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord; (this._discord.querySelector("span") as HTMLSpanElement).onclick = () => addDiscord(this.id);
} else { } else {
let innerHTML = ` let innerHTML = `
<div class="table-inline"> <div class="table-inline">
@ -283,6 +280,7 @@ class user implements User {
get discord_id(): string { return this._discordID; } get discord_id(): string { return this._discordID; }
set discord_id(id: string) { set discord_id(id: string) {
if (!window.discordEnabled || this._discordUsername == "") return;
this._discordID = id; this._discordID = id;
const link = this._discord.getElementsByClassName("discord-link")[0] as HTMLAnchorElement; const link = this._discord.getElementsByClassName("discord-link")[0] as HTMLAnchorElement;
link.href = `https://discord.com/users/${id}`; link.href = `https://discord.com/users/${id}`;
@ -412,76 +410,6 @@ class user implements User {
}); });
} }
private _timer: NodeJS.Timer;
private _discordKbListener = () => {
clearTimeout(this._timer);
const list = document.getElementById("discord-list") as HTMLTableElement;
const input = document.getElementById("discord-search") as HTMLInputElement;
if (input.value.length < 2) {
return;
}
list.innerHTML = ``;
addLoader(list);
list.parentElement.classList.add("mb-1", "mt-1");
this._timer = setTimeout(() => {
_get("/users/discord/" + input.value, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
removeLoader(list);
list.parentElement.classList.remove("mb-1", "mt-1");
return;
}
const users = req.response["users"] as Array<DiscordUser>;
let innerHTML = ``;
for (let i = 0; i < users.length; i++) {
innerHTML += `
<tr>
<td class="img-circle sm">
<img class="img-circle" src="${users[i].avatar_url}" width="32" height="32">
</td>
<td class="w-100 sm">
<p class="content">${users[i].name}</p>
</td>
<td class="sm">
<span id="discord-user-${users[i].id}" class="button ~info !high">${window.lang.strings("add")}</span>
</td>
</tr>
`;
}
list.innerHTML = innerHTML;
removeLoader(list);
list.parentElement.classList.remove("mb-1", "mt-1");
for (let i = 0; i < users.length; i++) {
const button = document.getElementById(`discord-user-${users[i].id}`) as HTMLInputElement;
button.onclick = () => _post("/users/discord", {jf_id: this.id, discord_id: users[i].id}, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
document.dispatchEvent(new CustomEvent("accounts-reload"));
if (req.status != 200) {
window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs"));
return
}
window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected"));
window.modals.discord.close()
}
});
}
}
});
}, 750);
}
private _addDiscord = () => {
if (!window.discordEnabled) { return; }
const input = document.getElementById("discord-search") as HTMLInputElement;
const list = document.getElementById("discord-list") as HTMLDivElement;
list.innerHTML = ``;
input.value = "";
input.removeEventListener("keyup", this._discordKbListener);
input.addEventListener("keyup", this._discordKbListener);
window.modals.discord.show();
}
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => { private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {
const pin = document.getElementById("telegram-pin"); const pin = document.getElementById("telegram-pin");
@ -530,15 +458,16 @@ class user implements User {
this.id = user.id; this.id = user.id;
this.name = user.name; this.name = user.name;
this.email = user.email || ""; this.email = user.email || "";
this.telegram = user.telegram;
this.discord = user.discord; this.discord = user.discord;
this.telegram = user.telegram;
this.last_active = user.last_active; this.last_active = user.last_active;
this.admin = user.admin; this.admin = user.admin;
this.disabled = user.disabled; this.disabled = user.disabled;
this.expiry = user.expiry; this.expiry = user.expiry;
this.notify_telegram = user.notify_telegram;
this.notify_discord = user.notify_discord; this.notify_discord = user.notify_discord;
this.notify_telegram = user.notify_telegram;
this.notify_email = user.notify_email; this.notify_email = user.notify_email;
this.discord_id = user.discord_id;
} }
asElement = (): HTMLTableRowElement => { return this._row; } asElement = (): HTMLTableRowElement => { return this._row; }
@ -1149,6 +1078,19 @@ export class accountsList {
}; };
this._announceTextarea.onkeyup = this.loadPreview; this._announceTextarea.onkeyup = this.loadPreview;
addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => {
_post("/users/discord", {jf_id: id, discord_id: user.id}, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
document.dispatchEvent(new CustomEvent("accounts-reload"));
if (req.status != 200) {
window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs"));
return
}
window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected"));
window.modals.discord.close()
}
});
});
} }
reload = () => _get("/users", null, (req: XMLHttpRequest) => { reload = () => _get("/users", null, (req: XMLHttpRequest) => {

79
ts/modules/discord.ts Normal file
View File

@ -0,0 +1,79 @@
import {addLoader, removeLoader, _get} from "../modules/common.js";
export interface DiscordUser {
name: string;
avatar_url: string;
id: string;
}
var listeners: { [buttonText: string]: (event: CustomEvent) => void } = {};
export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): (passData: string) => void {
if (!window.discordEnabled) {
return () => {};
}
let timer: NodeJS.Timer;
listeners[buttonText] = (event: CustomEvent) => {
clearTimeout(timer);
const list = document.getElementById("discord-list") as HTMLTableElement;
const input = document.getElementById("discord-search") as HTMLInputElement;
if (input.value.length < 2) {
return;
}
list.innerHTML = ``;
addLoader(list);
list.parentElement.classList.add("mb-1", "mt-1");
timer = setTimeout(() => {
_get("/users/discord/" + input.value, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
removeLoader(list);
list.parentElement.classList.remove("mb-1", "mt-1");
return;
}
const users = req.response["users"] as Array<DiscordUser>;
let innerHTML = ``;
for (let i = 0; i < users.length; i++) {
innerHTML += `
<tr>
<td class="img-circle sm">
<img class="img-circle" src="${users[i].avatar_url}" width="32" height="32">
</td>
<td class="w-100 sm">
<p class="content">${users[i].name}</p>
</td>
<td class="sm">
<span id="discord-user-${users[i].id}" class="button ~info !high">${buttonText}</span>
</td>
</tr>
`;
}
list.innerHTML = innerHTML;
removeLoader(list);
list.parentElement.classList.remove("mb-1", "mt-1");
for (let i = 0; i < users.length; i++) {
const button = document.getElementById(`discord-user-${users[i].id}`) as HTMLInputElement;
button.onclick = () => buttonFunction(users[i], event.detail);
}
}
});
}, 750);
}
return (passData: string) => {
const input = document.getElementById("discord-search") as HTMLInputElement;
const list = document.getElementById("discord-list") as HTMLDivElement;
const header = document.getElementById("discord-header") as HTMLSpanElement;
const desc = document.getElementById("discord-description") as HTMLParagraphElement;
desc.textContent = description;
header.textContent = title;
list.innerHTML = ``;
input.value = "";
for (let key in listeners) {
input.removeEventListener("keyup", listeners[key]);
}
input.addEventListener("keyup", listeners[buttonText].bind(null, { detail: passData }));
window.modals.discord.show();
}
}

View File

@ -1,4 +1,5 @@
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js"; import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
class DOMInvite implements Invite { class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => { updateNotify = (checkbox: HTMLInputElement) => {
@ -25,6 +26,7 @@ class DOMInvite implements Invite {
document.dispatchEvent(inviteDeletedEvent); document.dispatchEvent(inviteDeletedEvent);
} }
}) })
private _label: string = ""; private _label: string = "";
get label(): string { return this._label; } get label(): string { return this._label; }
set label(label: string) { set label(label: string) {
@ -82,10 +84,10 @@ class DOMInvite implements Invite {
this._middle.querySelector("strong.inv-remaining").textContent = remaining; this._middle.querySelector("strong.inv-remaining").textContent = remaining;
} }
private _email: string = ""; private _send_to: string = "";
get email(): string { return this._email }; get send_to(): string { return this._send_to };
set email(address: string) { set send_to(address: string) {
this._email = address; this._send_to = address;
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement; const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
const icon = container.querySelector("i"); const icon = container.querySelector("i");
const chip = container.querySelector("span.inv-email-chip"); const chip = container.querySelector("span.inv-email-chip");
@ -100,7 +102,7 @@ class DOMInvite implements Invite {
} else { } else {
container.classList.add("mr-1"); container.classList.add("mr-1");
chip.classList.add("chip"); chip.classList.add("chip");
if (address.includes("Failed to send to")) { if (address.includes("Failed")) {
icon.classList.remove("ri-mail-line"); icon.classList.remove("ri-mail-line");
icon.classList.add("ri-mail-close-line"); icon.classList.add("ri-mail-close-line");
chip.classList.remove("~neutral"); chip.classList.remove("~neutral");
@ -372,7 +374,7 @@ class DOMInvite implements Invite {
update = (invite: Invite) => { update = (invite: Invite) => {
this.code = invite.code; this.code = invite.code;
this.created = invite.created; this.created = invite.created;
this.email = invite.email; this.send_to = invite.send_to;
this.expiresIn = invite.expiresIn; this.expiresIn = invite.expiresIn;
if (window.notificationsEnabled) { if (window.notificationsEnabled) {
this.notifyCreation = invite.notifyCreation; this.notifyCreation = invite.notifyCreation;
@ -482,7 +484,7 @@ export class inviteList implements inviteList {
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite { function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
let parsed: Invite = {}; let parsed: Invite = {};
parsed.code = invite["code"] as string; parsed.code = invite["code"] as string;
parsed.email = invite["email"] as string || ""; parsed.send_to = invite["send_to"] as string || "";
parsed.label = invite["label"] as string || ""; parsed.label = invite["label"] as string || "";
let time = ""; let time = "";
let userExpiryTime = ""; let userExpiryTime = "";
@ -520,6 +522,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
export class createInvite { export class createInvite {
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement; private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement; private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
private _discordSearch: HTMLSpanElement;
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement; private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
private _uses = document.getElementById('create-uses') as HTMLInputElement; private _uses = document.getElementById('create-uses') as HTMLInputElement;
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement; private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
@ -542,6 +545,8 @@ export class createInvite {
private _invDuration = document.getElementById('inv-duration'); private _invDuration = document.getElementById('inv-duration');
private _userExpiry = document.getElementById('user-expiry'); private _userExpiry = document.getElementById('user-expiry');
private _sendToDiscord: (passData: string) => void;
// Broadcast when new invite created // Broadcast when new invite created
private _newInviteEvent = new CustomEvent("newInviteEvent"); private _newInviteEvent = new CustomEvent("newInviteEvent");
private _firstLoad = true; private _firstLoad = true;
@ -576,9 +581,19 @@ export class createInvite {
if (state) { if (state) {
this._sendToEnabled.parentElement.classList.remove("~neutral"); this._sendToEnabled.parentElement.classList.remove("~neutral");
this._sendToEnabled.parentElement.classList.add("~urge"); this._sendToEnabled.parentElement.classList.add("~urge");
if (window.discordEnabled) {
this._discordSearch.classList.remove("~neutral");
this._discordSearch.classList.add("~urge");
this._discordSearch.onclick = () => this._sendToDiscord("");
}
} else { } else {
this._sendToEnabled.parentElement.classList.remove("~urge"); this._sendToEnabled.parentElement.classList.remove("~urge");
this._sendToEnabled.parentElement.classList.add("~neutral"); this._sendToEnabled.parentElement.classList.add("~neutral");
if (window.discordEnabled) {
this._discordSearch.classList.remove("~urge");
this._discordSearch.classList.add("~neutral");
this._discordSearch.onclick = null;
}
} }
} }
@ -732,7 +747,7 @@ export class createInvite {
"multiple-uses": (this.uses > 1 || this.infiniteUses), "multiple-uses": (this.uses > 1 || this.infiniteUses),
"no-limit": this.infiniteUses, "no-limit": this.infiniteUses,
"remaining-uses": this.uses, "remaining-uses": this.uses,
"email": this.sendToEnabled ? this.sendTo : "", "send-to": this.sendToEnabled ? this.sendTo : "",
"profile": this.profile, "profile": this.profile,
"label": this.label "label": this.label
}; };
@ -761,7 +776,6 @@ export class createInvite {
this._userDays.disabled = true; this._userDays.disabled = true;
this._userHours.disabled = true; this._userHours.disabled = true;
this._userMinutes.disabled = true; this._userMinutes.disabled = true;
this.sendToEnabled = false;
this._createButton.onclick = this.create; this._createButton.onclick = this.create;
this.sendTo = ""; this.sendTo = "";
this.uses = 1; this.uses = 1;
@ -798,11 +812,22 @@ export class createInvite {
this._minutes.onchange = this._checkDurationValidity; this._minutes.onchange = this._checkDurationValidity;
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
if (!window.emailEnabled) { if (!window.emailEnabled && !window.discordEnabled) {
document.getElementById("create-send-to-container").classList.add("unfocused"); document.getElementById("create-send-to-container").classList.add("unfocused");
} }
if (window.discordEnabled) {
this._discordSearch = document.getElementById("create-send-to-search") as HTMLSpanElement;
this._sendToDiscord = newDiscordSearch(
window.lang.strings("findDiscordUser"),
window.lang.strings("searchDiscordUser"),
window.lang.strings("select"),
(user: DiscordUser) => {
this.sendTo = user.name;
window.modals.discord.close();
}
);
}
this.sendToEnabled = false;
} }
} }

View File

@ -109,7 +109,7 @@ interface Invite {
code?: string; code?: string;
expiresIn?: string; expiresIn?: string;
remainingUses?: string; remainingUses?: string;
email?: string; send_to?: string;
usedBy?: { [name: string]: number }; usedBy?: { [name: string]: number };
created?: number; created?: number;
notifyExpiry?: boolean; notifyExpiry?: boolean;

View File

@ -258,8 +258,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
} }
return return
} }
email := app.storage.invites[code].Email email := app.storage.invites[code].SendTo
if strings.Contains(email, "Failed") { if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = "" email = ""
} }
data := gin.H{ data := gin.H{
@ -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 {