mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10: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:
parent
524941da0c
commit
f8f5f35cc1
92
api.go
92
api.go
@ -330,6 +330,30 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
success = false
|
success = false
|
||||||
return
|
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
|
telegramTokenIndex := -1
|
||||||
if telegramEnabled {
|
if telegramEnabled {
|
||||||
if req.TelegramPIN == "" {
|
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)
|
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 {
|
if telegramEnabled && telegramTokenIndex != -1 {
|
||||||
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
|
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
|
||||||
tgUser := TelegramUser{
|
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 = map[string]TelegramUser{}
|
||||||
}
|
}
|
||||||
app.storage.telegram[user.ID] = tgUser
|
app.storage.telegram[user.ID] = tgUser
|
||||||
err := app.storage.storeTelegramUsers()
|
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("Failed to store Telegram users: %v", err)
|
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||||
} else {
|
} 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]
|
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)
|
name := app.getAddressOrName(user.ID)
|
||||||
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
||||||
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
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 {
|
if email, ok := app.storage.emails[jfUser.ID]; ok {
|
||||||
user.Email = email.(string)
|
user.Email = email.(string)
|
||||||
|
user.NotifyThroughEmail = user.Email != ""
|
||||||
}
|
}
|
||||||
expiry, ok := app.storage.users[jfUser.ID]
|
expiry, ok := app.storage.users[jfUser.ID]
|
||||||
if ok {
|
if ok {
|
||||||
@ -1178,6 +1213,11 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
user.Telegram = tgUser.Username
|
user.Telegram = tgUser.Username
|
||||||
user.NotifyThroughTelegram = tgUser.Contact
|
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
|
resp.UserList[i] = user
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@ -2011,7 +2051,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
|||||||
// @Router /users/telegram/notify [post]
|
// @Router /users/telegram/notify [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) TelegramSetNotify(gc *gin.Context) {
|
func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||||
var req telegramNotifyDTO
|
var req telegramNotifyDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
if req.ID == "" {
|
if req.ID == "" {
|
||||||
@ -2019,23 +2059,34 @@ func (app *appContext) TelegramSetNotify(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||||
tgUser.Contact = req.Enabled
|
tgUser.Contact = req.Telegram
|
||||||
app.storage.telegram[req.ID] = tgUser
|
app.storage.telegram[req.ID] = tgUser
|
||||||
if err := app.storage.storeTelegramUsers(); err != nil {
|
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
app.err.Printf("Telegram: Failed to store users: %v", err)
|
app.err.Printf("Telegram: Failed to store users: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respondBool(200, true, gc)
|
|
||||||
msg := ""
|
msg := ""
|
||||||
if !req.Enabled {
|
if !req.Telegram {
|
||||||
msg = "not"
|
msg = "not"
|
||||||
}
|
}
|
||||||
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
|
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)
|
if dcUser, ok := app.storage.discord[req.ID]; ok {
|
||||||
respondBool(400, false, gc)
|
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.
|
// @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)
|
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.
|
// @Summary Restarts the program. No response means success.
|
||||||
// @Router /restart [post]
|
// @Router /restart [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
|
@ -588,6 +588,15 @@
|
|||||||
"value": "!start",
|
"value": "!start",
|
||||||
"description": "Command to start the user verification process."
|
"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": {
|
"language": {
|
||||||
"name": "Language",
|
"name": "Language",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
156
discord.go
156
discord.go
@ -7,22 +7,17 @@ import (
|
|||||||
dg "github.com/bwmarrin/discordgo"
|
dg "github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscordToken struct {
|
|
||||||
Token string
|
|
||||||
ChannelID string
|
|
||||||
UserID string
|
|
||||||
Username string
|
|
||||||
}
|
|
||||||
|
|
||||||
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 map[string]DiscordToken // map of user IDs to tokens.
|
tokens []string
|
||||||
verifiedTokens []DiscordToken
|
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
|
||||||
languages map[string]string // Store of languages for user IDs. Added to on first interaction, and loaded from app.storage.discord on start.
|
channelID, channelName string
|
||||||
app *appContext
|
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) {
|
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||||
@ -38,28 +33,39 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
|||||||
Stopped: false,
|
Stopped: false,
|
||||||
ShutdownChannel: make(chan string),
|
ShutdownChannel: make(chan string),
|
||||||
bot: bot,
|
bot: bot,
|
||||||
tokens: map[string]DiscordToken{},
|
tokens: []string{},
|
||||||
verifiedTokens: []DiscordToken{},
|
verifiedTokens: map[string]DiscordUser{},
|
||||||
languages: map[string]string{},
|
users: map[string]DiscordUser{},
|
||||||
app: app,
|
app: app,
|
||||||
}
|
}
|
||||||
for _, user := range app.storage.discord {
|
for _, user := range app.storage.discord {
|
||||||
if user.Lang != "" {
|
dd.users[user.ID] = user
|
||||||
dd.languages[user.ID] = user.Lang
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dd, nil
|
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()
|
pin := genAuthToken()
|
||||||
token := DiscordToken{
|
d.tokens = append(d.tokens, pin)
|
||||||
Token: pin,
|
return pin
|
||||||
ChannelID: channelID,
|
}
|
||||||
UserID: userID,
|
|
||||||
Username: username,
|
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() {
|
func (d *DiscordDaemon) run() {
|
||||||
@ -70,6 +76,15 @@ func (d *DiscordDaemon) run() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
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
|
||||||
|
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()
|
defer d.bot.Close()
|
||||||
<-d.ShutdownChannel
|
<-d.ShutdownChannel
|
||||||
d.ShutdownChannel <- "Down"
|
d.ShutdownChannel <- "Down"
|
||||||
@ -84,6 +99,22 @@ func (d *DiscordDaemon) Shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
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 {
|
if m.Author.ID == s.State.User.ID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -92,11 +123,13 @@ func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lang := d.app.storage.lang.chosenTelegramLang
|
lang := d.app.storage.lang.chosenTelegramLang
|
||||||
if storedLang, ok := d.languages[m.Author.ID]; ok {
|
if user, ok := d.users[m.Author.ID]; ok {
|
||||||
lang = storedLang
|
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
|
||||||
|
lang = user.Lang
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch msg := sects[0]; msg {
|
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)
|
d.commandStart(s, m, lang)
|
||||||
case "!lang":
|
case "!lang":
|
||||||
d.commandLang(s, m, sects, 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)
|
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token := d.NewAuthToken(channel.ID, m.Author.ID, m.Author.Username)
|
user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
|
||||||
d.tokens[m.Author.ID] = token
|
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.get("startMessage") + "\n"
|
||||||
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
|
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
|
||||||
_, err = s.ChannelMessageSend(channel.ID, content)
|
_, err = s.ChannelMessageSend(channel.ID, content)
|
||||||
@ -139,7 +172,7 @@ func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
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 {
|
for jfID, user := range d.app.storage.discord {
|
||||||
if user.ID == m.Author.ID {
|
if user.ID == m.Author.ID {
|
||||||
user.Lang = sects[1]
|
user.Lang = sects[1]
|
||||||
@ -150,30 +183,69 @@ func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
d.users[m.Author.ID] = user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||||
token, ok := d.tokens[m.Author.ID]
|
if _, ok := d.users[m.Author.ID]; ok {
|
||||||
if !ok || token.Token != sects[0] {
|
channel, err := s.Channel(m.ChannelID)
|
||||||
_, err := s.ChannelMessageSendReply(
|
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,
|
m.ChannelID,
|
||||||
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||||
m.Reference(),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := s.ChannelMessageSendReply(
|
_, err := s.ChannelMessageSend(
|
||||||
m.ChannelID,
|
m.ChannelID,
|
||||||
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||||
m.Reference(),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
}
|
}
|
||||||
d.verifiedTokens = append(d.verifiedTokens, token)
|
d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
|
||||||
delete(d.tokens, 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
|
||||||
}
|
}
|
||||||
|
22
email.go
22
email.go
@ -230,7 +230,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
|||||||
var keys []string
|
var keys []string
|
||||||
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
|
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
|
||||||
if plaintext {
|
if plaintext {
|
||||||
if telegramEnabled {
|
if telegramEnabled || discordEnabled {
|
||||||
keys = []string{"text"}
|
keys = []string{"text"}
|
||||||
text, markdown = "", ""
|
text, markdown = "", ""
|
||||||
} else {
|
} else {
|
||||||
@ -238,7 +238,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
|||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if telegramEnabled {
|
if telegramEnabled || discordEnabled {
|
||||||
keys = []string{"html", "text", "markdown"}
|
keys = []string{"html", "text", "markdown"}
|
||||||
} else {
|
} else {
|
||||||
keys = []string{"html", "text"}
|
keys = []string{"html", "text"}
|
||||||
@ -807,8 +807,21 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
|
|||||||
var err error
|
var err error
|
||||||
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
||||||
err = app.telegram.Send(email, tgChat.ChatID)
|
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))
|
err = app.email.send(email, address.(string))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -818,6 +831,9 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) getAddressOrName(jfID string) string {
|
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 {
|
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
|
||||||
return "@" + tgChat.Username
|
return "@" + tgChat.Username
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
window.notificationsEnabled = {{ .notifications }};
|
window.notificationsEnabled = {{ .notifications }};
|
||||||
window.emailEnabled = {{ .email_enabled }};
|
window.emailEnabled = {{ .email_enabled }};
|
||||||
window.telegramEnabled = {{ .telegram_enabled }};
|
window.telegramEnabled = {{ .telegram_enabled }};
|
||||||
|
window.discordEnabled = {{ .discord_enabled }};
|
||||||
window.ombiEnabled = {{ .ombiEnabled }};
|
window.ombiEnabled = {{ .ombiEnabled }};
|
||||||
window.usernameEnabled = {{ .username }};
|
window.usernameEnabled = {{ .username }};
|
||||||
window.langFile = JSON.parse({{ .language }});
|
window.langFile = JSON.parse({{ .language }});
|
||||||
@ -525,6 +526,9 @@
|
|||||||
{{ if .telegram_enabled }}
|
{{ if .telegram_enabled }}
|
||||||
<th>Telegram</th>
|
<th>Telegram</th>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .discord_enabled }}
|
||||||
|
<th>Discord</th>
|
||||||
|
{{ end }}
|
||||||
<th>{{ .strings.expiry }}</th>
|
<th>{{ .strings.expiry }}</th>
|
||||||
<th>{{ .strings.lastActiveTime }}</th>
|
<th>{{ .strings.lastActiveTime }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -17,6 +17,9 @@
|
|||||||
window.telegramEnabled = {{ .telegramEnabled }};
|
window.telegramEnabled = {{ .telegramEnabled }};
|
||||||
window.telegramRequired = {{ .telegramRequired }};
|
window.telegramRequired = {{ .telegramRequired }};
|
||||||
window.telegramPIN = "{{ .telegramPIN }}";
|
window.telegramPIN = "{{ .telegramPIN }}";
|
||||||
|
window.discordEnabled = {{ .discordEnabled }};
|
||||||
|
window.discordRequired = {{ .discordRequired }};
|
||||||
|
window.discordPIN = "{{ .discordPIN }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="js/form.js" type="module"></script>
|
<script src="js/form.js" type="module"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -37,6 +37,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ 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="dropdown" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
<i class="ri-global-line"></i>
|
<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 }}">
|
<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 }}
|
{{ if .telegramEnabled }}
|
||||||
<span class="button ~info !normal full-width center mb-1" id="link-telegram">{{ .strings.linkTelegram }}</span>
|
<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">
|
<div id="contact-via" class="unfocused">
|
||||||
<label class="row switch pb-1">
|
<label class="row switch pb-1">
|
||||||
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
|
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
|
||||||
</label>
|
</label>
|
||||||
|
{{ if .telegramEnabled }}
|
||||||
<label class="row switch pb-1">
|
<label class="row switch pb-1">
|
||||||
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
|
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
"linkTelegram": "Link Telegram",
|
"linkTelegram": "Link Telegram",
|
||||||
"contactEmail": "Contact through Email",
|
"contactEmail": "Contact through Email",
|
||||||
"contactTelegram": "Contact through Telegram",
|
"contactTelegram": "Contact through Telegram",
|
||||||
|
"linkDiscord": "Link Discord",
|
||||||
|
"contactDiscord": "Contact through Discord",
|
||||||
"theme": "Theme"
|
"theme": "Theme"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,14 +18,16 @@
|
|||||||
"confirmationRequired": "Email confirmation required",
|
"confirmationRequired": "Email confirmation required",
|
||||||
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
|
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
|
||||||
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
"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": {
|
"notifications": {
|
||||||
"errorUserExists": "User already exists.",
|
"errorUserExists": "User already exists.",
|
||||||
"errorInvalidCode": "Invalid invite code.",
|
"errorInvalidCode": "Invalid invite code.",
|
||||||
"errorTelegramVerification": "Telegram verification required.",
|
"errorTelegramVerification": "Telegram verification required.",
|
||||||
"errorInvalidPIN": "Telegram PIN is invalid.",
|
"errorDiscordVerification": "Discord verification required.",
|
||||||
"telegramVerified": "Telegram account verified."
|
"errorInvalidPIN": "PIN is invalid.",
|
||||||
|
"verified": "Account verified."
|
||||||
},
|
},
|
||||||
"validationStrings": {
|
"validationStrings": {
|
||||||
"length": {
|
"length": {
|
||||||
|
22
models.go
22
models.go
@ -17,6 +17,8 @@ type newUserDTO struct {
|
|||||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
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)
|
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
|
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 {
|
type newUserResponse struct {
|
||||||
@ -125,12 +127,16 @@ type respUser struct {
|
|||||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||||
Name string `json:"name" example:"jeff"` // Username 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)
|
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
|
NotifyThroughEmail bool `json:"notify_email"`
|
||||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||||
Telegram string `json:"telegram"` // Telegram username (if known)
|
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||||
|
Telegram string `json:"telegram"` // Telegram username (if known)
|
||||||
NotifyThroughTelegram bool `json:"notify_telegram"`
|
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 {
|
type getUsersDTO struct {
|
||||||
@ -250,6 +256,8 @@ type telegramSetDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type telegramNotifyDTO struct {
|
type telegramNotifyDTO struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Enabled bool `json:"enabled"`
|
Email bool `json:"email"`
|
||||||
|
Discord bool `json:"discord"`
|
||||||
|
Telegram bool `json:"telegram"`
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
if telegramEnabled {
|
if telegramEnabled {
|
||||||
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
||||||
}
|
}
|
||||||
|
if discordEnabled {
|
||||||
|
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if *SWAGGER {
|
if *SWAGGER {
|
||||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
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/pin", app.TelegramGetPin)
|
||||||
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
|
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
|
||||||
api.POST(p+"/users/telegram", app.TelegramAddUser)
|
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) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||||
|
11
storage.go
11
storage.go
@ -39,11 +39,12 @@ type TelegramUser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiscordUser struct {
|
type DiscordUser struct {
|
||||||
ChannelID string
|
ChannelID string
|
||||||
ID string
|
ID string
|
||||||
Username string
|
Username string
|
||||||
Lang string
|
Discriminator string
|
||||||
Contact bool
|
Lang string
|
||||||
|
Contact bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type customEmails struct {
|
type customEmails struct {
|
||||||
|
55
ts/form.ts
55
ts/form.ts
@ -8,12 +8,16 @@ interface formWindow extends Window {
|
|||||||
invalidPassword: string;
|
invalidPassword: string;
|
||||||
successModal: Modal;
|
successModal: Modal;
|
||||||
telegramModal: Modal;
|
telegramModal: Modal;
|
||||||
|
discordModal: Modal;
|
||||||
confirmationModal: Modal
|
confirmationModal: Modal
|
||||||
code: string;
|
code: string;
|
||||||
messages: { [key: string]: string };
|
messages: { [key: string]: string };
|
||||||
confirmation: boolean;
|
confirmation: boolean;
|
||||||
telegramRequired: boolean;
|
telegramRequired: boolean;
|
||||||
telegramPIN: string;
|
telegramPIN: string;
|
||||||
|
discordRequired: boolean;
|
||||||
|
discordPIN: string;
|
||||||
|
discordStartCommand: string;
|
||||||
userExpiryEnabled: boolean;
|
userExpiryEnabled: boolean;
|
||||||
userExpiryMonths: number;
|
userExpiryMonths: number;
|
||||||
userExpiryDays: number;
|
userExpiryDays: number;
|
||||||
@ -68,7 +72,7 @@ if (window.telegramEnabled) {
|
|||||||
telegramVerified = true;
|
telegramVerified = true;
|
||||||
waiting.classList.add("~positive");
|
waiting.classList.add("~positive");
|
||||||
waiting.classList.remove("~info");
|
waiting.classList.remove("~info");
|
||||||
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
|
window.notifications.customPositive("telegramVerified", "", window.messages["verified"]);
|
||||||
setTimeout(window.telegramModal.close, 2000);
|
setTimeout(window.telegramModal.close, 2000);
|
||||||
telegramButton.classList.add("unfocused");
|
telegramButton.classList.add("unfocused");
|
||||||
document.getElementById("contact-via").classList.remove("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) {
|
if (window.confirmation) {
|
||||||
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
|
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
|
||||||
}
|
}
|
||||||
@ -161,6 +205,8 @@ interface sendDTO {
|
|||||||
password: string;
|
password: string;
|
||||||
telegram_pin?: string;
|
telegram_pin?: string;
|
||||||
telegram_contact?: boolean;
|
telegram_contact?: boolean;
|
||||||
|
discord_pin?: string;
|
||||||
|
discord_contact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = (event: SubmitEvent) => {
|
const create = (event: SubmitEvent) => {
|
||||||
@ -179,6 +225,13 @@ const create = (event: SubmitEvent) => {
|
|||||||
send.telegram_contact = true;
|
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) => {
|
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
let vals = req.response as respDTO;
|
let vals = req.response as respDTO;
|
||||||
|
@ -7,12 +7,16 @@ interface User {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string | undefined;
|
email: string | undefined;
|
||||||
|
notify_email: boolean;
|
||||||
last_active: number;
|
last_active: number;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
expiry: number;
|
expiry: number;
|
||||||
telegram: string;
|
telegram: string;
|
||||||
notify_telegram: boolean;
|
notify_telegram: boolean;
|
||||||
|
discord: string;
|
||||||
|
notify_discord: boolean;
|
||||||
|
discord_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface getPinResponse {
|
interface getPinResponse {
|
||||||
@ -27,11 +31,16 @@ class user implements User {
|
|||||||
private _admin: HTMLSpanElement;
|
private _admin: HTMLSpanElement;
|
||||||
private _disabled: HTMLSpanElement;
|
private _disabled: HTMLSpanElement;
|
||||||
private _email: HTMLInputElement;
|
private _email: HTMLInputElement;
|
||||||
|
private _notifyEmail: boolean;
|
||||||
private _emailAddress: string;
|
private _emailAddress: string;
|
||||||
private _emailEditButton: HTMLElement;
|
private _emailEditButton: HTMLElement;
|
||||||
private _telegram: HTMLTableDataCellElement;
|
private _telegram: HTMLTableDataCellElement;
|
||||||
private _telegramUsername: string;
|
private _telegramUsername: string;
|
||||||
private _notifyTelegram: boolean;
|
private _notifyTelegram: boolean;
|
||||||
|
private _discord: HTMLTableDataCellElement;
|
||||||
|
private _discordUsername: string;
|
||||||
|
private _discordID: string;
|
||||||
|
private _notifyDiscord: boolean;
|
||||||
private _expiry: HTMLTableDataCellElement;
|
private _expiry: HTMLTableDataCellElement;
|
||||||
private _expiryUnix: number;
|
private _expiryUnix: number;
|
||||||
private _lastActive: HTMLTableDataCellElement;
|
private _lastActive: HTMLTableDataCellElement;
|
||||||
@ -82,6 +91,19 @@ class user implements User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
get telegram(): string { return this._telegramUsername; }
|
||||||
set telegram(u: string) {
|
set telegram(u: string) {
|
||||||
if (!window.telegramEnabled) return;
|
if (!window.telegramEnabled) return;
|
||||||
@ -90,7 +112,7 @@ class user implements User {
|
|||||||
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
|
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
|
||||||
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
|
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
|
||||||
} else {
|
} else {
|
||||||
this._telegram.innerHTML = `
|
let innerHTML = `
|
||||||
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
||||||
<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">
|
||||||
@ -98,23 +120,34 @@ class user implements User {
|
|||||||
<div class="card ~neutral !low">
|
<div class="card ~neutral !low">
|
||||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||||
<label class="switch pb-1 mt-half">
|
<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>
|
<span>Email</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="switch pb-1">
|
<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>
|
<span>Telegram</span>
|
||||||
</label>
|
</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>
|
</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.
|
// 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;
|
||||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||||
for (let i = 0; i < radios.length; i++) {
|
for (let i = 0; i < checks.length; i++) {
|
||||||
radios[i].onclick = this._setTelegramNotify;
|
checks[i].onclick = () => this._setNotifyMethod("telegram");
|
||||||
}
|
}
|
||||||
|
|
||||||
button.onclick = () => {
|
button.onclick = () => {
|
||||||
@ -134,37 +167,129 @@ class user implements User {
|
|||||||
set notify_telegram(s: boolean) {
|
set notify_telegram(s: boolean) {
|
||||||
if (!window.telegramEnabled || !this._telegramUsername) return;
|
if (!window.telegramEnabled || !this._telegramUsername) return;
|
||||||
this._notifyTelegram = s;
|
this._notifyTelegram = s;
|
||||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
|
||||||
radios[0].checked = !s;
|
telegram.checked = s;
|
||||||
radios[1].checked = s;
|
if (window.discordEnabled && this._discordUsername != "") {
|
||||||
|
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
|
||||||
|
telegram.checked = s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setTelegramNotify = () => {
|
private _setNotifyMethod = (mode: string = "telegram") => {
|
||||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
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 = {
|
let send = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
enabled: radios[1].checked
|
email: email.checked
|
||||||
};
|
}
|
||||||
_post("/users/telegram/notify", send, (req: XMLHttpRequest) => {
|
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.readyState == 4) {
|
||||||
if (req.status != 200) {
|
if (req.status != 200) {
|
||||||
window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings"));
|
window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings"));
|
||||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
document.dispatchEvent(new CustomEvent("accounts-reload"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, false, (req: XMLHttpRequest) => {
|
}, false, (req: XMLHttpRequest) => {
|
||||||
if (req.status == 0) {
|
if (req.status == 0) {
|
||||||
window.notifications.connectionError();
|
window.notifications.connectionError();
|
||||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
document.dispatchEvent(new CustomEvent("accounts-reload"));
|
||||||
return;
|
return;
|
||||||
} else if (req.status == 401) {
|
} 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"));
|
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; }
|
get expiry(): number { return this._expiryUnix; }
|
||||||
set expiry(unix: number) {
|
set expiry(unix: number) {
|
||||||
this._expiryUnix = unix;
|
this._expiryUnix = unix;
|
||||||
@ -200,6 +325,11 @@ class user implements User {
|
|||||||
<td class="accounts-telegram"></td>
|
<td class="accounts-telegram"></td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
innerHTML += `
|
||||||
|
<td class="accounts-discord"></td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<td class="accounts-expiry"></td>
|
<td class="accounts-expiry"></td>
|
||||||
<td class="accounts-last-active"></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._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
|
||||||
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
||||||
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
|
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._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
||||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||||
@ -320,11 +451,14 @@ class user implements User {
|
|||||||
this.name = user.name;
|
this.name = user.name;
|
||||||
this.email = user.email || "";
|
this.email = user.email || "";
|
||||||
this.telegram = user.telegram;
|
this.telegram = user.telegram;
|
||||||
|
this.discord = user.discord;
|
||||||
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_telegram = user.notify_telegram;
|
||||||
|
this.notify_discord = user.notify_discord;
|
||||||
|
this.notify_email = user.notify_email;
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||||
|
@ -21,6 +21,7 @@ declare interface Window {
|
|||||||
notificationsEnabled: boolean;
|
notificationsEnabled: boolean;
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
telegramEnabled: boolean;
|
telegramEnabled: boolean;
|
||||||
|
discordEnabled: boolean;
|
||||||
ombiEnabled: boolean;
|
ombiEnabled: boolean;
|
||||||
usernameEnabled: boolean;
|
usernameEnabled: boolean;
|
||||||
token: string;
|
token: string;
|
||||||
|
19
views.go
19
views.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -121,6 +122,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
|||||||
"contactMessage": "",
|
"contactMessage": "",
|
||||||
"email_enabled": emailEnabled,
|
"email_enabled": emailEnabled,
|
||||||
"telegram_enabled": telegramEnabled,
|
"telegram_enabled": telegramEnabled,
|
||||||
|
"discord_enabled": discordEnabled,
|
||||||
"notifications": notificationsEnabled,
|
"notifications": notificationsEnabled,
|
||||||
"version": version,
|
"version": version,
|
||||||
"commit": commit,
|
"commit": commit,
|
||||||
@ -284,13 +286,28 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
|
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
|
||||||
"langName": lang,
|
"langName": lang,
|
||||||
"telegramEnabled": telegramEnabled,
|
"telegramEnabled": telegramEnabled,
|
||||||
|
"discordEnabled": discordEnabled,
|
||||||
}
|
}
|
||||||
if data["telegramEnabled"].(bool) {
|
if telegramEnabled {
|
||||||
data["telegramPIN"] = app.telegram.NewAuthToken()
|
data["telegramPIN"] = app.telegram.NewAuthToken()
|
||||||
data["telegramUsername"] = app.telegram.username
|
data["telegramUsername"] = app.telegram.username
|
||||||
data["telegramURL"] = app.telegram.link
|
data["telegramURL"] = app.telegram.link
|
||||||
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
|
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)
|
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user