1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

PIN verification, notifications, multiple notif providers

Discord, Email & Telegram can be enabled, although email is always
enabled right now (will fix). Also apparently markdown hyperlinks don't
work in Discord, eventually will implement something to convert them to
embeds.
This commit is contained in:
Harvey Tindall 2021-05-21 21:35:25 +01:00
parent 524941da0c
commit f8f5f35cc1
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
16 changed files with 509 additions and 92 deletions

92
api.go
View File

@ -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 == "" {
@ -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)
@ -1169,6 +1203,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
}
if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.(string)
user.NotifyThroughEmail = user.Email != ""
}
expiry, ok := app.storage.users[jfUser.ID]
if ok {
@ -1178,6 +1213,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++
}
@ -2011,7 +2051,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Router /users/telegram/notify [post]
// @Security Bearer
// @tags Other
func (app *appContext) TelegramSetNotify(gc *gin.Context) {
func (app *appContext) SetContactMethods(gc *gin.Context) {
var req telegramNotifyDTO
gc.BindJSON(&req)
if req.ID == "" {
@ -2019,23 +2059,34 @@ func (app *appContext) TelegramSetNotify(gc *gin.Context) {
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 {
if !req.Telegram {
msg = "not"
}
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
return
}
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)
}
respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
@ -2092,6 +2143,25 @@ 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
// @Success 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 Restarts the program. No response means success.
// @Router /restart [post]
// @Security Bearer

View File

@ -588,6 +588,15 @@
"value": "!start",
"description": "Command to start the user verification process."
},
"channel": {
"name": "Channel to monitor",
"required": false,
"requires_restart": true,
"depens_true": "enabled",
"type": "text",
"value": "",
"description": "Only listen to commands in specified channel. Leave blank to monitor all."
},
"language": {
"name": "Language",
"required": false,

View File

@ -7,22 +7,17 @@ import (
dg "github.com/bwmarrin/discordgo"
)
type DiscordToken struct {
Token string
ChannelID string
UserID string
Username string
}
type DiscordDaemon struct {
Stopped bool
ShutdownChannel chan string
bot *dg.Session
username string
tokens map[string]DiscordToken // map of user IDs to tokens.
verifiedTokens []DiscordToken
languages map[string]string // Store of languages for user IDs. Added to on first interaction, and loaded from app.storage.discord on start.
app *appContext
Stopped bool
ShutdownChannel chan string
bot *dg.Session
username string
tokens []string
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
channelID, channelName string
serverChannelName string
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
app *appContext
}
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
@ -38,28 +33,39 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
Stopped: false,
ShutdownChannel: make(chan string),
bot: bot,
tokens: map[string]DiscordToken{},
verifiedTokens: []DiscordToken{},
languages: map[string]string{},
tokens: []string{},
verifiedTokens: map[string]DiscordUser{},
users: map[string]DiscordUser{},
app: app,
}
for _, user := range app.storage.discord {
if user.Lang != "" {
dd.languages[user.ID] = user.Lang
}
dd.users[user.ID] = user
}
return dd, nil
}
func (d *DiscordDaemon) NewAuthToken(channelID, userID, username string) DiscordToken {
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (d *DiscordDaemon) NewAuthToken() string {
pin := genAuthToken()
token := DiscordToken{
Token: pin,
ChannelID: channelID,
UserID: userID,
Username: username,
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 token
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() {
@ -70,6 +76,15 @@ func (d *DiscordDaemon) run() {
return
}
d.username = d.bot.State.User.Username
// Choose the last guild (server), for now we don't really support multiple anyway
guild, err := d.bot.Guild(d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID)
if err != nil {
d.app.err.Printf("Discord: Failed to get guild: %v", err)
}
d.serverChannelName = guild.Name
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
d.channelName = channel
}
defer d.bot.Close()
<-d.ShutdownChannel
d.ShutdownChannel <- "Down"
@ -84,6 +99,22 @@ func (d *DiscordDaemon) Shutdown() {
}
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
}
@ -92,11 +123,13 @@ func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
return
}
lang := d.app.storage.lang.chosenTelegramLang
if storedLang, ok := d.languages[m.Author.ID]; ok {
lang = storedLang
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("telegram").Key("start_command").MustString("!start"):
case d.app.config.Section("discord").Key("start_command").MustString("!start"):
d.commandStart(s, m, lang)
case "!lang":
d.commandLang(s, m, sects, lang)
@ -111,8 +144,8 @@ func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang st
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
return
}
token := d.NewAuthToken(channel.ID, m.Author.ID, m.Author.Username)
d.tokens[m.Author.ID] = token
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)
@ -139,7 +172,7 @@ func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []
return
}
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
d.languages[m.Author.ID] = sects[1]
var user DiscordUser
for jfID, user := range d.app.storage.discord {
if user.ID == m.Author.ID {
user.Lang = sects[1]
@ -150,30 +183,69 @@ func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []
break
}
}
d.users[m.Author.ID] = user
}
}
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
token, ok := d.tokens[m.Author.ID]
if !ok || token.Token != sects[0] {
_, err := s.ChannelMessageSendReply(
if _, ok := d.users[m.Author.ID]; ok {
channel, err := s.Channel(m.ChannelID)
if err != nil {
d.app.err.Printf("Discord: Failed to get channel: %v", err)
return
}
if channel.Type != dg.ChannelTypeDM {
d.app.debug.Println("Discord: Ignoring message as not a DM")
return
}
} else {
d.app.debug.Println("Discord: Ignoring message as user was not found")
return
}
tokenIndex := -1
for i, token := range d.tokens {
if sects[0] == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
_, err := s.ChannelMessageSend(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
m.Reference(),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
return
}
_, err := s.ChannelMessageSendReply(
_, err := s.ChannelMessageSend(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
m.Reference(),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
d.verifiedTokens = append(d.verifiedTokens, token)
delete(d.tokens, m.Author.ID)
d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
d.tokens = d.tokens[:len(d.tokens)-1]
}
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
for _, id := range channelID {
msg := ""
if message.Markdown == "" {
msg = message.Text
} else {
msg = message.Markdown
}
_, err := d.bot.ChannelMessageSend(
id,
msg,
)
if err != nil {
return err
}
}
return nil
}

View File

@ -230,7 +230,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
var keys []string
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
if plaintext {
if telegramEnabled {
if telegramEnabled || discordEnabled {
keys = []string{"text"}
text, markdown = "", ""
} else {
@ -238,7 +238,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
text = ""
}
} else {
if telegramEnabled {
if telegramEnabled || discordEnabled {
keys = []string{"html", "text", "markdown"}
} else {
keys = []string{"html", "text"}
@ -807,8 +807,21 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
var err error
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID)
} else if address, ok := app.storage.emails[id]; ok {
if err != nil {
return err
}
}
if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
err = app.discord.Send(email, dcChat.ChannelID)
if err != nil {
return err
}
}
if address, ok := app.storage.emails[id]; ok {
err = app.email.send(email, address.(string))
if err != nil {
return err
}
}
if err != nil {
return err
@ -818,6 +831,9 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
}
func (app *appContext) getAddressOrName(jfID string) string {
if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
return dcChat.Username + "#" + dcChat.Discriminator
}
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
return "@" + tgChat.Username
}

View File

@ -7,6 +7,7 @@
window.notificationsEnabled = {{ .notifications }};
window.emailEnabled = {{ .email_enabled }};
window.telegramEnabled = {{ .telegram_enabled }};
window.discordEnabled = {{ .discord_enabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
@ -525,6 +526,9 @@
{{ if .telegram_enabled }}
<th>Telegram</th>
{{ end }}
{{ if .discord_enabled }}
<th>Discord</th>
{{ end }}
<th>{{ .strings.expiry }}</th>
<th>{{ .strings.lastActiveTime }}</th>
</tr>

View File

@ -17,6 +17,9 @@
window.telegramEnabled = {{ .telegramEnabled }};
window.telegramRequired = {{ .telegramRequired }};
window.telegramPIN = "{{ .telegramPIN }}";
window.discordEnabled = {{ .discordEnabled }};
window.discordRequired = {{ .discordRequired }};
window.discordPIN = "{{ .discordPIN }}";
</script>
<script src="js/form.js" type="module"></script>
{{ end }}

View File

@ -37,6 +37,16 @@
</div>
</div>
{{ end }}
{{ if .discordEnabled }}
<div id="modal-discord" class="modal">
<div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkDiscord }}</span>
<p class="content mb-1"> {{ .discordSendPINMessage }}</p>
<h1 class="ac">{{ .discordPIN }}</h1>
<span class="button ~info !normal full-width center mt-1" id="discord-waiting">{{ .strings.success }}</span>
</div>
</div>
{{ end }}
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
@ -69,13 +79,25 @@
<input type="email" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
{{ if .telegramEnabled }}
<span class="button ~info !normal full-width center mb-1" id="link-telegram">{{ .strings.linkTelegram }}</span>
{{ end }}
{{ if .discordEnabled }}
<span class="button ~info !normal full-width center mb-1" id="link-discord">{{ .strings.linkDiscord }}</span>
{{ end }}
{{ if or (.telegramEnabled) (.discordEnabled) }}
<div id="contact-via" class="unfocused">
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
</label>
{{ if .telegramEnabled }}
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
</label>
{{ end }}
{{ if .discordEnabled }}
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="discord" id="contact-via-discord"><span>Contact through Discord</span>
</label>
{{ end }}
</div>
{{ end }}
<label class="label supra" for="create-password">{{ .strings.password }}</label>

View File

@ -17,6 +17,8 @@
"linkTelegram": "Link Telegram",
"contactEmail": "Contact through Email",
"contactTelegram": "Contact through Telegram",
"linkDiscord": "Link Discord",
"contactDiscord": "Contact through Discord",
"theme": "Theme"
}
}

View File

@ -18,14 +18,16 @@
"confirmationRequired": "Email confirmation required",
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
"sendPIN": "Send the PIN below to the bot, then come back here to link your account."
"sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below via DM to the bot."
},
"notifications": {
"errorUserExists": "User already exists.",
"errorInvalidCode": "Invalid invite code.",
"errorTelegramVerification": "Telegram verification required.",
"errorInvalidPIN": "Telegram PIN is invalid.",
"telegramVerified": "Telegram account verified."
"errorDiscordVerification": "Discord verification required.",
"errorInvalidPIN": "PIN is invalid.",
"verified": "Account verified."
},
"validationStrings": {
"length": {

View File

@ -17,6 +17,8 @@ type newUserDTO struct {
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
DiscordPIN string `json:"discord_pin" example:"A1-B2-3C"` // Discord verification PIN (if used)
DiscordContact bool `json:"discord_contact"` // Whether or not to use discord for notifications/pwrs
}
type newUserResponse struct {
@ -125,12 +127,16 @@ type respUser struct {
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
Name string `json:"name" example:"jeff"` // Username of user
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
Telegram string `json:"telegram"` // Telegram username (if known)
NotifyThroughEmail bool `json:"notify_email"`
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
Telegram string `json:"telegram"` // Telegram username (if known)
NotifyThroughTelegram bool `json:"notify_telegram"`
Discord string `json:"discord"` // Discord username (if known)
DiscordID string `json:"discord_id"` // Discord user ID for creating links.
NotifyThroughDiscord bool `json:"notify_discord"`
}
type getUsersDTO struct {
@ -250,6 +256,8 @@ type telegramSetDTO struct {
}
type telegramNotifyDTO struct {
ID string `json:"id"`
Enabled bool `json:"enabled"`
ID string `json:"id"`
Email bool `json:"email"`
Discord bool `json:"discord"`
Telegram bool `json:"telegram"`
}

View File

@ -121,6 +121,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if telegramEnabled {
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
}
if discordEnabled {
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
}
}
if *SWAGGER {
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
@ -162,7 +165,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
api.POST(p+"/users/contact", app.SetContactMethods)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)

View File

@ -39,11 +39,12 @@ type TelegramUser struct {
}
type DiscordUser struct {
ChannelID string
ID string
Username string
Lang string
Contact bool
ChannelID string
ID string
Username string
Discriminator string
Lang string
Contact bool
}
type customEmails struct {

View File

@ -8,12 +8,16 @@ interface formWindow extends Window {
invalidPassword: string;
successModal: Modal;
telegramModal: Modal;
discordModal: Modal;
confirmationModal: Modal
code: string;
messages: { [key: string]: string };
confirmation: boolean;
telegramRequired: boolean;
telegramPIN: string;
discordRequired: boolean;
discordPIN: string;
discordStartCommand: string;
userExpiryEnabled: boolean;
userExpiryMonths: number;
userExpiryDays: number;
@ -68,7 +72,7 @@ if (window.telegramEnabled) {
telegramVerified = true;
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
window.notifications.customPositive("telegramVerified", "", window.messages["verified"]);
setTimeout(window.telegramModal.close, 2000);
telegramButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
@ -84,6 +88,46 @@ if (window.telegramEnabled) {
};
}
var discordVerified = false;
if (window.discordEnabled) {
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
discordButton.onclick = () => {
const waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
toggleLoader(waiting);
window.discordModal.show();
let modalClosed = false;
window.discordModal.onclose = () => {
modalClosed = true;
toggleLoader(waiting);
}
const checkVerified = () => _get("/invite/" + window.code + "/discord/verified/" + window.discordPIN, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 401) {
window.discordModal.close();
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
return;
} else if (req.status == 200) {
if (req.response["success"] as boolean) {
discordVerified = true;
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customPositive("discordVerified", "", window.messages["verified"]);
setTimeout(window.discordModal.close, 2000);
discordButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
radio.checked = true;
} else if (!modalClosed) {
setTimeout(checkVerified, 1500);
}
}
}
});
checkVerified();
};
}
if (window.confirmation) {
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
}
@ -161,6 +205,8 @@ interface sendDTO {
password: string;
telegram_pin?: string;
telegram_contact?: boolean;
discord_pin?: string;
discord_contact?: boolean;
}
const create = (event: SubmitEvent) => {
@ -179,6 +225,13 @@ const create = (event: SubmitEvent) => {
send.telegram_contact = true;
}
}
if (discordVerified) {
send.discord_pin = window.discordPIN;
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
if (radio.checked) {
send.discord_contact = true;
}
}
_post("/newUser", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
let vals = req.response as respDTO;

View File

@ -7,12 +7,16 @@ interface User {
id: string;
name: string;
email: string | undefined;
notify_email: boolean;
last_active: number;
admin: boolean;
disabled: boolean;
expiry: number;
telegram: string;
notify_telegram: boolean;
discord: string;
notify_discord: boolean;
discord_id: string;
}
interface getPinResponse {
@ -27,11 +31,16 @@ class user implements User {
private _admin: HTMLSpanElement;
private _disabled: HTMLSpanElement;
private _email: HTMLInputElement;
private _notifyEmail: boolean;
private _emailAddress: string;
private _emailEditButton: HTMLElement;
private _telegram: HTMLTableDataCellElement;
private _telegramUsername: string;
private _notifyTelegram: boolean;
private _discord: HTMLTableDataCellElement;
private _discordUsername: string;
private _discordID: string;
private _notifyDiscord: boolean;
private _expiry: HTMLTableDataCellElement;
private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement;
@ -81,6 +90,19 @@ class user implements User {
this._email.textContent = value;
}
}
get notify_email(): boolean { return this._notifyEmail; }
set notify_email(s: boolean) {
this._notifyEmail = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
email.checked = s;
}
if (window.discordEnabled && this._discordUsername != "") {
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
email.checked = s;
}
}
get telegram(): string { return this._telegramUsername; }
set telegram(u: string) {
@ -90,7 +112,7 @@ class user implements User {
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
this._telegram.innerHTML = `
let innerHTML = `
<a href="https://t.me/${u}" target="_blank">@${u}</a>
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
@ -98,23 +120,34 @@ class user implements User {
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="switch pb-1 mt-half">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="switch pb-1">
<input type="radio" name="accounts-contact-${this.id}">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
`;
if (window.discordEnabled && this._discordUsername != "") {
innerHTML += `
<label class="switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<span>Discord</span>
</label>
`;
}
innerHTML += `
</div>
</div>
</div>
`;
this._discord.innerHTML = innerHTML;
// 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 dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < radios.length; i++) {
radios[i].onclick = this._setTelegramNotify;
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod("telegram");
}
button.onclick = () => {
@ -134,36 +167,128 @@ class user implements User {
set notify_telegram(s: boolean) {
if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s;
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
radios[0].checked = !s;
radios[1].checked = s;
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s;
if (window.discordEnabled && this._discordUsername != "") {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s;
}
}
private _setTelegramNotify = () => {
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
private _setNotifyMethod = (mode: string = "telegram") => {
let el: HTMLElement;
if (mode == "telegram") { el = this._telegram }
else if (mode == "discord") { el = this._discord }
const email = el.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
let send = {
id: this.id,
enabled: radios[1].checked
};
_post("/users/telegram/notify", send, (req: XMLHttpRequest) => {
email: email.checked
}
if (window.telegramEnabled && this._telegramUsername != "") {
const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
send["telegram"] = telegram.checked;
}
if (window.discordEnabled && this._discordUsername != "") {
const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
send["discord"] = discord.checked;
}
_post("/users/contact", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings"));
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings"));
document.dispatchEvent(new CustomEvent("accounts-reload"));
return;
}
}
}, false, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.connectionError();
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
document.dispatchEvent(new CustomEvent("accounts-reload"));
return;
} else if (req.status == 401) {
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
document.dispatchEvent(new CustomEvent("accounts-reload"));
}
});
}
get discord(): string { return this._discordUsername; }
set discord(u: string) {
if (!window.discordEnabled) return;
this._discordUsername = u;
if (u == "") {
this._discord.innerHTML = `<span class="chip btn !low">Add</span>`;
// (this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
} else {
let innerHTML = `
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">@${u}</a>
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown-display">
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="switch pb-1 mt-half">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="switch pb-1">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<span>Discord</span>
</label>
`;
if (window.telegramEnabled && this._telegramUsername != "") {
innerHTML += `
<label class="switch pb-1">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
`;
}
innerHTML += `
</div>
</div>
</div>
`;
this._discord.innerHTML = innerHTML;
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
const button = this._discord.querySelector("i");
const dropdown = this._discord.querySelector("div.dropdown") as HTMLDivElement;
const checks = this._discord.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod("discord");
}
button.onclick = () => {
dropdown.classList.add("selected");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._discord.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
}
}
get discord_id(): string { return this._discordID; }
set discord_id(id: string) {
this._discordID = id;
const link = this._discord.getElementsByClassName("discord-link")[0] as HTMLAnchorElement;
link.href = `https://discord.com/users/${id}`;
}
get notify_discord(): boolean { return this._notifyDiscord; }
set notify_discord(s: boolean) {
if (!window.discordEnabled || !this._discordUsername) return;
this._notifyDiscord = s;
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
discord.checked = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
discord.checked = s;
}
}
get expiry(): number { return this._expiryUnix; }
set expiry(unix: number) {
@ -200,6 +325,11 @@ class user implements User {
<td class="accounts-telegram"></td>
`;
}
if (window.discordEnabled) {
innerHTML += `
<td class="accounts-discord"></td>
`;
}
innerHTML += `
<td class="accounts-expiry"></td>
<td class="accounts-last-active"></td>
@ -213,6 +343,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; }
@ -320,11 +451,14 @@ class user implements User {
this.name = user.name;
this.email = user.email || "";
this.telegram = user.telegram;
this.discord = user.discord;
this.last_active = user.last_active;
this.admin = user.admin;
this.disabled = user.disabled;
this.expiry = user.expiry;
this.notify_telegram = user.notify_telegram;
this.notify_discord = user.notify_discord;
this.notify_email = user.notify_email;
}
asElement = (): HTMLTableRowElement => { return this._row; }

View File

@ -21,6 +21,7 @@ declare interface Window {
notificationsEnabled: boolean;
emailEnabled: boolean;
telegramEnabled: boolean;
discordEnabled: boolean;
ombiEnabled: boolean;
usernameEnabled: boolean;
token: string;

View File

@ -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,
@ -284,13 +286,28 @@ 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": `<code class="code">` + app.config.Section("discord").Key("start_command").MustString("!start") + `</code>`,
"server_channel": app.discord.serverChannelName,
}))
}
// if discordEnabled {
// pin := ""
// for _, token := range app.discord.tokens {
// if
gcHTML(gc, http.StatusOK, "form-loader.html", data)
}