diff --git a/api.go b/api.go
index 6888868..4c1ccb2 100644
--- a/api.go
+++ b/api.go
@@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
username := jfUser.Name
email := ""
if e, ok := app.storage.emails[jfID]; ok {
- email = e.(string)
+ email = e.Addr
}
for _, ombiUser := range ombiUsers {
ombiAddr := ""
@@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
app.jf.CacheExpiry = time.Now()
if emailEnabled {
- app.storage.emails[id] = req.Email
+ app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
@@ -330,6 +330,30 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
+ var discordUser DiscordUser
+ discordVerified := false
+ if discordEnabled {
+ if req.DiscordPIN == "" {
+ if app.config.Section("discord").Key("required").MustBool(false) {
+ f = func(gc *gin.Context) {
+ app.debug.Printf("%s: New user failed: Discord verification not completed", req.Code)
+ respond(401, "errorDiscordVerification", gc)
+ }
+ success = false
+ return
+ }
+ } else {
+ discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
+ if !discordVerified {
+ f = func(gc *gin.Context) {
+ app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
+ respond(401, "errorInvalidPIN", gc)
+ }
+ success = false
+ return
+ }
+ }
+ }
telegramTokenIndex := -1
if telegramEnabled {
if req.TelegramPIN == "" {
@@ -454,7 +478,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" {
- app.storage.emails[id] = req.Email
+ app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
@@ -479,7 +503,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.err.Printf("Failed to store user duration: %v", err)
}
}
-
+ if discordEnabled && discordVerified {
+ discordUser.Contact = req.DiscordContact
+ if app.storage.discord == nil {
+ app.storage.discord = map[string]DiscordUser{}
+ }
+ app.storage.discord[user.ID] = discordUser
+ if err := app.storage.storeDiscordUsers(); err != nil {
+ app.err.Printf("Failed to store Discord users: %v", err)
+ } else {
+ delete(app.discord.verifiedTokens, req.DiscordPIN)
+ }
+ }
if telegramEnabled && telegramTokenIndex != -1 {
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
tgUser := TelegramUser{
@@ -494,8 +529,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.storage.telegram = map[string]TelegramUser{}
}
app.storage.telegram[user.ID] = tgUser
- err := app.storage.storeTelegramUsers()
- if err != nil {
+ if err := app.storage.storeTelegramUsers(); err != nil {
app.err.Printf("Failed to store Telegram users: %v", err)
} else {
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
@@ -503,7 +537,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
}
- if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 {
+ if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
name := app.getAddressOrName(user.ID)
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
@@ -792,18 +826,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.UserMinutes = req.UserMinutes
}
invite.ValidTill = validTill
- if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
- app.debug.Printf("%s: Sending invite email", inviteCode)
- invite.Email = req.Email
- msg, err := app.email.constructInvite(inviteCode, invite, app, false)
- if err != nil {
- invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
- app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err)
- } else if err := app.email.send(msg, req.Email); err != nil {
- invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
- app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
- } else {
- app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email)
+ if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
+ addressValid := false
+ discord := ""
+ app.debug.Printf("%s: Sending invite message", inviteCode)
+ if discordEnabled && !strings.Contains(req.SendTo, "@") {
+ users := app.discord.GetUsers(req.SendTo)
+ if len(users) == 0 {
+ invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
+ } else if len(users) > 1 {
+ invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
+ } else {
+ 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 != "" {
@@ -867,15 +927,15 @@ func (app *appContext) GetInvites(gc *gin.Context) {
if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses
}
- if inv.Email != "" {
- invite.Email = inv.Email
+ if inv.SendTo != "" {
+ invite.SendTo = inv.SendTo
}
if len(inv.Notify) != 0 {
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails()
- if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
- address = addr.(string)
+ if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
+ address = addr.Addr
}
} else {
address = app.config.Section("ui").Key("email").String()
@@ -1074,14 +1134,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
}
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
- var ok bool
- address, ok = app.storage.emails[gc.GetString("jfId")].(string)
+ addr, ok := app.storage.emails[gc.GetString("jfId")]
if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user email", gc)
return
}
+ address = addr.Addr
} else {
address = app.config.Section("ui").Key("email").String()
}
@@ -1168,7 +1228,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.LastActive = jfUser.LastActivityDate.Unix()
}
if email, ok := app.storage.emails[jfUser.ID]; ok {
- user.Email = email.(string)
+ user.Email = email.Addr
+ user.NotifyThroughEmail = email.Contact
}
expiry, ok := app.storage.users[jfUser.ID]
if ok {
@@ -1178,6 +1239,11 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact
}
+ if dc, ok := app.storage.discord[jfUser.ID]; ok {
+ user.Discord = dc.Username + "#" + dc.Discriminator
+ user.DiscordID = dc.ID
+ user.NotifyThroughDiscord = dc.Contact
+ }
resp.UserList[i] = user
i++
}
@@ -1253,7 +1319,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
for _, jfUser := range users {
id := jfUser.ID
if address, ok := req[id]; ok {
- app.storage.emails[id] = address
+ contact := true
+ if oldAddr, ok := app.storage.emails[id]; ok {
+ contact = oldAddr.Contact
+ }
+ app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil {
@@ -2004,38 +2074,63 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Summary Sets whether to notify a user through telegram or not.
// @Produce json
-// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
+// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse
// @Router /users/telegram/notify [post]
// @Security Bearer
// @tags Other
-func (app *appContext) TelegramSetNotify(gc *gin.Context) {
- var req telegramNotifyDTO
+func (app *appContext) SetContactMethods(gc *gin.Context) {
+ var req SetContactMethodsDTO
gc.BindJSON(&req)
if req.ID == "" {
respondBool(400, false, gc)
return
}
if tgUser, ok := app.storage.telegram[req.ID]; ok {
- tgUser.Contact = req.Enabled
+ tgUser.Contact = req.Telegram
app.storage.telegram[req.ID] = tgUser
if err := app.storage.storeTelegramUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Telegram: Failed to store users: %v", err)
return
}
- respondBool(200, true, gc)
msg := ""
- if !req.Enabled {
- msg = "not"
+ if !req.Telegram {
+ msg = " not"
}
- app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
- return
+ app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
}
- app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
- respondBool(400, false, gc)
+ if dcUser, ok := app.storage.discord[req.ID]; ok {
+ dcUser.Contact = req.Discord
+ app.storage.discord[req.ID] = dcUser
+ if err := app.storage.storeDiscordUsers(); err != nil {
+ respondBool(500, false, gc)
+ app.err.Printf("Discord: Failed to store users: %v", err)
+ return
+ }
+ msg := ""
+ if !req.Discord {
+ msg = " not"
+ }
+ app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
+ }
+ if email, ok := app.storage.emails[req.ID]; ok {
+ email.Contact = req.Email
+ app.storage.emails[req.ID] = email
+ if err := app.storage.storeEmails(); err != nil {
+ respondBool(500, false, gc)
+ app.err.Printf("Failed to store emails: %v", err)
+ return
+ }
+ msg := ""
+ if !req.Email {
+ msg = " not"
+ }
+ app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
+ }
+ respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
@@ -2092,6 +2187,107 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
respondBool(200, tokenIndex != -1, gc)
}
+// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
+// @Produce json
+// @Success 200 {object} boolResponse
+// @Failure 401 {object} boolResponse
+// @Param pin path string true "PIN code to check"
+// @Param invCode path string true "invite Code"
+// @Router /invite/{invCode}/discord/verified/{pin} [get]
+// @tags Other
+func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
+ code := gc.Param("invCode")
+ if _, ok := app.storage.invites[code]; !ok {
+ respondBool(401, false, gc)
+ return
+ }
+ pin := gc.Param("pin")
+ _, ok := app.discord.verifiedTokens[pin]
+ 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
+// @Failure 400 {object} boolResponse
+// @Failure 500 {object} boolResponse
+// @Param username path string true "username to search."
+// @Router /users/discord/{username} [get]
+// @tags Other
+func (app *appContext) DiscordGetUsers(gc *gin.Context) {
+ name := gc.Param("username")
+ if name == "" {
+ respondBool(400, false, gc)
+ return
+ }
+ users := app.discord.GetUsers(name)
+ resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
+ for i, u := range users {
+ resp.Users[i] = DiscordUserDTO{
+ Name: u.User.Username + "#" + u.User.Discriminator,
+ ID: u.User.ID,
+ AvatarURL: u.User.AvatarURL("32"),
+ }
+ }
+ gc.JSON(200, resp)
+}
+
+// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
+// @Produce json
+// @Success 200 {object} boolResponse
+// @Failure 400 {object} boolResponse
+// @Failure 500 {object} boolResponse
+// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
+// @Router /users/discord [post]
+// @tags Other
+func (app *appContext) DiscordConnect(gc *gin.Context) {
+ var req DiscordConnectUserDTO
+ gc.BindJSON(&req)
+ if req.JellyfinID == "" || req.DiscordID == "" {
+ respondBool(400, false, gc)
+ return
+ }
+ user, ok := app.discord.NewUser(req.DiscordID)
+ if !ok {
+ respondBool(500, false, gc)
+ return
+ }
+ app.storage.discord[req.JellyfinID] = user
+ if err := app.storage.storeDiscordUsers(); err != nil {
+ app.err.Printf("Failed to store Discord users: %v", err)
+ respondBool(500, false, gc)
+ return
+ }
+ respondBool(200, true, gc)
+}
+
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
// @Security Bearer
diff --git a/config.go b/config.go
index 9c60efc..d605ef5 100644
--- a/config.go
+++ b/config.go
@@ -14,6 +14,7 @@ import (
var emailEnabled = false
var messagesEnabled = false
var telegramEnabled = false
+var discordEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("")
@@ -42,7 +43,7 @@ func (app *appContext) loadConfig() error {
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
}
}
- for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
+ for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users"} {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
}
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
@@ -87,15 +88,17 @@ func (app *appContext) loadConfig() error {
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
+ discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
if !messagesEnabled {
emailEnabled = false
telegramEnabled = false
+ discordEnabled = false
} else if app.config.Section("email").Key("method").MustString("") == "" {
emailEnabled = false
} else {
emailEnabled = true
}
- if !emailEnabled && !telegramEnabled {
+ if !emailEnabled && !telegramEnabled && !discordEnabled {
messagesEnabled = false
}
@@ -168,3 +171,28 @@ func (app *appContext) migrateEmailConfig() {
}
app.loadConfig()
}
+
+func (app *appContext) migrateEmailStorage() error {
+ var emails map[string]interface{}
+ err := loadJSON(app.storage.emails_path, &emails)
+ if err != nil {
+ return err
+ }
+ newEmails := map[string]EmailAddress{}
+ for jfID, addr := range emails {
+ newEmails[jfID] = EmailAddress{
+ Addr: addr.(string),
+ Contact: true,
+ }
+ }
+ err = storeJSON(app.storage.emails_path+".bak", emails)
+ if err != nil {
+ return err
+ }
+ err = storeJSON(app.storage.emails_path, newEmails)
+ if err != nil {
+ return err
+ }
+ app.info.Println("Migrated to new email format. A backup has also been made.")
+ return nil
+}
diff --git a/config/config-base.json b/config/config-base.json
index 12f6861..36bad28 100644
--- a/config/config-base.json
+++ b/config/config-base.json
@@ -546,6 +546,89 @@
}
}
},
+ "discord": {
+ "order": [],
+ "meta": {
+ "name": "Discord",
+ "description": "Settings for Discord invites/signup/notifications"
+ },
+ "settings": {
+ "enabled": {
+ "name": "Enabled",
+ "required": false,
+ "requires_restart": true,
+ "type": "bool",
+ "value": false,
+ "description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
+ },
+ "required": {
+ "name": "Require on sign-up",
+ "required": false,
+ "required_restart": true,
+ "depends_true": "enabled",
+ "type": "bool",
+ "value": false,
+ "description": "Require Discord connection on sign-up."
+ },
+ "token": {
+ "name": "API Token",
+ "required": false,
+ "requires_restart": true,
+ "depends_true": "enabled",
+ "type": "text",
+ "value": "",
+ "description": "Discord Bot API Token."
+ },
+ "start_command": {
+ "name": "Start command",
+ "required": false,
+ "requires_restart": true,
+ "depends_true": "enabled",
+ "type": "text",
+ "value": "!start",
+ "description": "Command to start the user verification process."
+ },
+ "channel": {
+ "name": "Channel to monitor",
+ "required": false,
+ "requires_restart": true,
+ "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,
+ "requires_restart": false,
+ "depends_true": "enabled",
+ "type": "select",
+ "options": [
+ ["en-us", "English (US)"]
+ ],
+ "value": "en-us",
+ "description": "Default Discord message language. Visit weblate if you'd like to translate."
+ }
+ }
+ },
"telegram": {
"order": [],
"meta": {
@@ -565,6 +648,7 @@
"name": "Require on sign-up",
"required": false,
"required_restart": true,
+ "depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Require telegram connection on sign-up."
@@ -1140,6 +1224,14 @@
"type": "text",
"value": "",
"description": "Stores telegram user IDs and language preferences."
+ },
+ "discord_users": {
+ "name": "Discord users",
+ "required": false,
+ "requires_restart": false,
+ "type": "text",
+ "value": "",
+ "description": "Stores discord user IDs and language preferences."
}
}
}
diff --git a/css/base.css b/css/base.css
index 4b1e040..7d96684 100644
--- a/css/base.css
+++ b/css/base.css
@@ -30,12 +30,12 @@
}
}
-@media screen and (max-width: 750px) {
+@media screen and (max-width: 1000px) {
:root {
font-size: 0.9rem;
}
.table-responsive table {
- min-width: 660px;
+ min-width: 800px;
}
}
@@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) {
text-align: center;
}
+.w-100 {
+ width: 100%;
+}
+
.inline-block {
display: inline-block;
}
@@ -172,7 +176,7 @@ div.card:contains(section.banner.footer) {
}
p.sm,
-span.sm {
+span.sm:not(.heading) {
font-size: 0.75rem;
}
@@ -424,6 +428,7 @@ p.top {
.table-responsive {
overflow-x: auto;
+ font-size: 0.9rem;
}
#notification-box {
@@ -438,6 +443,10 @@ p.top {
margin-bottom: -0.5rem;
}
+.dropdown-display.lg {
+ white-space: nowrap;
+}
+
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
@@ -483,3 +492,32 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
max-width: 15rem;
min-width: 10rem;
}
+
+td.img-circle {
+ width: 32px;
+ height: 32px;
+}
+
+span.img-circle.lg {
+ width: 64px;
+ height: 64px;
+}
+
+span.shield.img-circle {
+ padding: 0.2rem;
+}
+
+img.img-circle {
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+.table td.sm {
+ padding-top: 0.1rem;
+ padding-bottom: 0.1rem;
+}
+
+.table-inline {
+ display: flex !important;
+ align-items: center;
+}
diff --git a/discord.go b/discord.go
new file mode 100644
index 0000000..43c2ebf
--- /dev/null
+++ b/discord.go
@@ -0,0 +1,404 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ dg "github.com/bwmarrin/discordgo"
+)
+
+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, 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) {
+ 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: []string{},
+ verifiedTokens: map[string]DiscordUser{},
+ users: map[string]DiscordUser{},
+ app: app,
+ }
+ for _, user := range app.storage.discord {
+ 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 = append(d.tokens, pin)
+ 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.messageHandler)
+ 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
+ }
+ // Sometimes bot.State isn't populated quick enough
+ for d.bot.State == nil {
+ continue
+ }
+ d.username = d.bot.State.User.Username
+ // 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("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.guildID,
+ "",
+ 1000,
+ )
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to get members: %v", err)
+ return nil
+ }
+ hasDiscriminator := strings.Contains(username, "#")
+ var users []*dg.Member
+ for _, member := range members {
+ if hasDiscriminator {
+ if member.User.Username+"#"+member.User.Discriminator == username {
+ 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("Discord: Failed to get user: %v", 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("Discord: Failed to create DM channel: %v", 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) messageHandler(s *dg.Session, m *dg.MessageCreate) {
+ if m.GuildID != "" && d.channelName != "" {
+ if d.channelID == "" {
+ channel, err := s.Channel(m.ChannelID)
+ if err != nil {
+ d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
+ d.channelName = ""
+ }
+ if channel.Name == d.channelName {
+ d.channelID = channel.ID
+ }
+ }
+ if d.channelID != m.ChannelID {
+ d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
+ return
+ }
+ }
+ if m.Author.ID == s.State.User.ID {
+ return
+ }
+ sects := strings.Split(m.Content, " ")
+ if len(sects) == 0 {
+ return
+ }
+ lang := d.app.storage.lang.chosenTelegramLang
+ if user, ok := d.users[m.Author.ID]; ok {
+ if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
+ lang = user.Lang
+ }
+ }
+ switch msg := sects[0]; msg {
+ case d.app.config.Section("discord").Key("start_command").MustString("!start"):
+ d.commandStart(s, m, lang)
+ case "!lang":
+ d.commandLang(s, m, sects, lang)
+ default:
+ d.commandPIN(s, m, sects, lang)
+ }
+}
+
+func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang string) {
+ channel, err := s.UserChannelCreate(m.Author.ID)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
+ return
+ }
+ user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
+ d.users[m.Author.ID] = user
+ content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
+ content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
+ _, err = s.ChannelMessageSend(channel.ID, content)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
+ return
+ }
+}
+
+func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
+ if len(sects) == 1 {
+ list := "!lang
") + stripped, _ := StripAltText(md, false) + return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "
"), "")
}
diff --git a/telegram.go b/telegram.go
index 050086c..1975757 100644
--- a/telegram.go
+++ b/telegram.go
@@ -9,7 +9,7 @@ import (
tg "github.com/go-telegram-bot-api/telegram-bot-api"
)
-type VerifiedToken struct {
+type TelegramVerifiedToken struct {
Token string
ChatID int64
Username string
@@ -21,7 +21,7 @@ type TelegramDaemon struct {
bot *tg.BotAPI
username string
tokens []string
- verifiedTokens []VerifiedToken
+ verifiedTokens []TelegramVerifiedToken
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
link string
app *appContext
@@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return nil, err
}
td := &TelegramDaemon{
- Stopped: false,
ShutdownChannel: make(chan string),
bot: bot,
username: bot.Self.UserName,
tokens: []string{},
- verifiedTokens: []VerifiedToken{},
+ verifiedTokens: []TelegramVerifiedToken{},
languages: map[int64]string{},
link: "https://t.me/" + bot.Self.UserName,
app: app,
@@ -55,10 +54,7 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return td, nil
}
-var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
-
-// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
-func (t *TelegramDaemon) NewAuthToken() string {
+func genAuthToken() string {
rand.Seed(time.Now().UnixNano())
pin := make([]rune, 8)
for i := range pin {
@@ -68,10 +64,18 @@ func (t *TelegramDaemon) NewAuthToken() string {
pin[i] = runes[rand.Intn(len(runes))]
}
}
- t.tokens = append(t.tokens, string(pin))
return string(pin)
}
+var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
+func (t *TelegramDaemon) NewAuthToken() string {
+ pin := genAuthToken()
+ t.tokens = append(t.tokens, pin)
+ return pin
+}
+
func (t *TelegramDaemon) run() {
t.app.info.Println("Starting Telegram bot daemon")
u := tg.NewUpdate(0)
@@ -79,6 +83,7 @@ func (t *TelegramDaemon) run() {
updates, err := t.bot.GetUpdatesChan(u)
if err != nil {
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
+ telegramEnabled = false
return
}
for {
@@ -171,7 +176,7 @@ func (t *TelegramDaemon) Shutdown() {
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
- content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
+ content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
err := t.Reply(upd, content)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
@@ -180,9 +185,9 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
if len(sects) == 1 {
- list := "/lang ${users[i].name}
-
-
+
+
`;
if (window.telegramEnabled) {
innerHTML += `
`;
}
+ if (window.discordEnabled) {
+ innerHTML += `
+
+ `;
+ }
innerHTML += `
@@ -213,6 +351,7 @@ class user implements User {
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
+ this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._check.onchange = () => { this.selected = this._check.checked; }
@@ -270,7 +409,7 @@ class user implements User {
}
});
}
-
+
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) {
const pin = document.getElementById("telegram-pin");
@@ -319,12 +458,16 @@ class user implements User {
this.id = user.id;
this.name = user.name;
this.email = user.email || "";
+ this.discord = user.discord;
this.telegram = user.telegram;
this.last_active = user.last_active;
this.admin = user.admin;
this.disabled = user.disabled;
this.expiry = user.expiry;
+ this.notify_discord = user.notify_discord;
this.notify_telegram = user.notify_telegram;
+ this.notify_email = user.notify_email;
+ this.discord_id = user.discord_id;
}
asElement = (): HTMLTableRowElement => { return this._row; }
@@ -935,6 +1078,19 @@ export class accountsList {
};
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) => {
diff --git a/ts/modules/discord.ts b/ts/modules/discord.ts
new file mode 100644
index 0000000..ea8138a
--- /dev/null
+++ b/ts/modules/discord.ts
@@ -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
+
+ `;
+ }
+ 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();
+ }
+}
diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts
index 4645755..e43cd50 100644
--- a/ts/modules/invites.ts
+++ b/ts/modules/invites.ts
@@ -1,4 +1,5 @@
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
+import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => {
@@ -25,6 +26,7 @@ class DOMInvite implements Invite {
document.dispatchEvent(inviteDeletedEvent);
}
})
+
private _label: string = "";
get label(): string { return this._label; }
set label(label: string) {
@@ -82,10 +84,10 @@ class DOMInvite implements Invite {
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
}
- private _email: string = "";
- get email(): string { return this._email };
- set email(address: string) {
- this._email = address;
+ private _send_to: string = "";
+ get send_to(): string { return this._send_to };
+ set send_to(address: string) {
+ this._send_to = address;
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
const icon = container.querySelector("i");
const chip = container.querySelector("span.inv-email-chip");
@@ -100,7 +102,7 @@ class DOMInvite implements Invite {
} else {
container.classList.add("mr-1");
chip.classList.add("chip");
- if (address.includes("Failed to send to")) {
+ if (address.includes("Failed")) {
icon.classList.remove("ri-mail-line");
icon.classList.add("ri-mail-close-line");
chip.classList.remove("~neutral");
@@ -372,7 +374,7 @@ class DOMInvite implements Invite {
update = (invite: Invite) => {
this.code = invite.code;
this.created = invite.created;
- this.email = invite.email;
+ this.send_to = invite.send_to;
this.expiresIn = invite.expiresIn;
if (window.notificationsEnabled) {
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 {
let parsed: Invite = {};
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 || "";
let time = "";
let userExpiryTime = "";
@@ -520,6 +522,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
export class createInvite {
private _sendToEnabled = document.getElementById("create-send-to-enabled") 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 _uses = document.getElementById('create-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 _userExpiry = document.getElementById('user-expiry');
+ private _sendToDiscord: (passData: string) => void;
+
// Broadcast when new invite created
private _newInviteEvent = new CustomEvent("newInviteEvent");
private _firstLoad = true;
@@ -576,9 +581,19 @@ export class createInvite {
if (state) {
this._sendToEnabled.parentElement.classList.remove("~neutral");
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 {
this._sendToEnabled.parentElement.classList.remove("~urge");
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),
"no-limit": this.infiniteUses,
"remaining-uses": this.uses,
- "email": this.sendToEnabled ? this.sendTo : "",
+ "send-to": this.sendToEnabled ? this.sendTo : "",
"profile": this.profile,
"label": this.label
};
@@ -761,7 +776,6 @@ export class createInvite {
this._userDays.disabled = true;
this._userHours.disabled = true;
this._userMinutes.disabled = true;
- this.sendToEnabled = false;
this._createButton.onclick = this.create;
this.sendTo = "";
this.uses = 1;
@@ -798,11 +812,22 @@ export class createInvite {
this._minutes.onchange = this._checkDurationValidity;
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
- if (!window.emailEnabled) {
+ if (!window.emailEnabled && !window.discordEnabled) {
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;
}
}
-
-
-
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index 7e0bf3e..eb5be55 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -21,6 +21,7 @@ declare interface Window {
notificationsEnabled: boolean;
emailEnabled: boolean;
telegramEnabled: boolean;
+ discordEnabled: boolean;
ombiEnabled: boolean;
usernameEnabled: boolean;
token: string;
@@ -101,13 +102,14 @@ declare interface Modals {
extendExpiry: Modal;
updateInfo: Modal;
telegram: Modal;
+ discord: Modal;
}
interface Invite {
code?: string;
expiresIn?: string;
remainingUses?: string;
- email?: string;
+ send_to?: string;
usedBy?: { [name: string]: number };
created?: number;
notifyExpiry?: boolean;
diff --git a/views.go b/views.go
index 8841c09..6d8e306 100644
--- a/views.go
+++ b/views.go
@@ -1,6 +1,7 @@
package main
import (
+ "html/template"
"io/fs"
"net/http"
"strconv"
@@ -121,6 +122,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"contactMessage": "",
"email_enabled": emailEnabled,
"telegram_enabled": telegramEnabled,
+ "discord_enabled": discordEnabled,
"notifications": notificationsEnabled,
"version": version,
"commit": commit,
@@ -256,8 +258,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
}
return
}
- email := app.storage.invites[code].Email
- if strings.Contains(email, "Failed") {
+ email := app.storage.invites[code].SendTo
+ if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = ""
}
data := gin.H{
@@ -284,13 +286,30 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"telegramEnabled": telegramEnabled,
+ "discordEnabled": discordEnabled,
}
- if data["telegramEnabled"].(bool) {
+ if telegramEnabled {
data["telegramPIN"] = app.telegram.NewAuthToken()
data["telegramUsername"] = app.telegram.username
data["telegramURL"] = app.telegram.link
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
}
+ if discordEnabled {
+ data["discordPIN"] = app.discord.NewAuthToken()
+ data["discordUsername"] = app.discord.username
+ data["discordRequired"] = app.config.Section("discord").Key("required").MustBool(false)
+ data["discordSendPINMessage"] = template.HTML(app.storage.lang.Form[lang].Strings.template("sendPINDiscord", tmpl{
+ "command": `
+
+
+
+
+
+ ${buttonText}
+
+ ` + 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 {
+ // pin := ""
+ // for _, token := range app.discord.tokens {
+ // if
gcHTML(gc, http.StatusOK, "form-loader.html", data)
}