Merge Discord branch
Discord Integration, Accounts UI improvements
260
api.go
@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
|||||||
username := jfUser.Name
|
username := jfUser.Name
|
||||||
email := ""
|
email := ""
|
||||||
if e, ok := app.storage.emails[jfID]; ok {
|
if e, ok := app.storage.emails[jfID]; ok {
|
||||||
email = e.(string)
|
email = e.Addr
|
||||||
}
|
}
|
||||||
for _, ombiUser := range ombiUsers {
|
for _, ombiUser := range ombiUsers {
|
||||||
ombiAddr := ""
|
ombiAddr := ""
|
||||||
@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
app.jf.CacheExpiry = time.Now()
|
app.jf.CacheExpiry = time.Now()
|
||||||
if emailEnabled {
|
if emailEnabled {
|
||||||
app.storage.emails[id] = req.Email
|
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||||
app.storage.storeEmails()
|
app.storage.storeEmails()
|
||||||
}
|
}
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
@ -330,6 +330,30 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
success = false
|
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 == "" {
|
||||||
@ -454,7 +478,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
}
|
}
|
||||||
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||||
if req.Email != "" {
|
if req.Email != "" {
|
||||||
app.storage.emails[id] = req.Email
|
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||||
app.storage.storeEmails()
|
app.storage.storeEmails()
|
||||||
}
|
}
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
@ -479,7 +503,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
app.err.Printf("Failed to store user duration: %v", err)
|
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)
|
||||||
@ -792,18 +826,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
invite.UserMinutes = req.UserMinutes
|
invite.UserMinutes = req.UserMinutes
|
||||||
}
|
}
|
||||||
invite.ValidTill = validTill
|
invite.ValidTill = validTill
|
||||||
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||||
app.debug.Printf("%s: Sending invite email", inviteCode)
|
addressValid := false
|
||||||
invite.Email = req.Email
|
discord := ""
|
||||||
|
app.debug.Printf("%s: Sending invite message", inviteCode)
|
||||||
|
if discordEnabled && !strings.Contains(req.SendTo, "@") {
|
||||||
|
users := app.discord.GetUsers(req.SendTo)
|
||||||
|
if len(users) == 0 {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||||
|
} else if len(users) > 1 {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||||
|
} else {
|
||||||
|
invite.SendTo = req.SendTo
|
||||||
|
addressValid = true
|
||||||
|
discord = users[0].User.ID
|
||||||
|
}
|
||||||
|
} else if emailEnabled {
|
||||||
|
addressValid = true
|
||||||
|
invite.SendTo = req.SendTo
|
||||||
|
}
|
||||||
|
if addressValid {
|
||||||
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err)
|
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
|
||||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
|
||||||
app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
|
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email)
|
var err error
|
||||||
|
if discord != "" {
|
||||||
|
err = app.discord.SendDM(msg, discord)
|
||||||
|
} else {
|
||||||
|
err = app.email.send(msg, req.SendTo)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
|
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Profile != "" {
|
if req.Profile != "" {
|
||||||
@ -867,15 +927,15 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
if inv.RemainingUses != 0 {
|
if inv.RemainingUses != 0 {
|
||||||
invite.RemainingUses = inv.RemainingUses
|
invite.RemainingUses = inv.RemainingUses
|
||||||
}
|
}
|
||||||
if inv.Email != "" {
|
if inv.SendTo != "" {
|
||||||
invite.Email = inv.Email
|
invite.SendTo = inv.SendTo
|
||||||
}
|
}
|
||||||
if len(inv.Notify) != 0 {
|
if len(inv.Notify) != 0 {
|
||||||
var address string
|
var address string
|
||||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||||
app.storage.loadEmails()
|
app.storage.loadEmails()
|
||||||
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
|
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
|
||||||
address = addr.(string)
|
address = addr.Addr
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
address = app.config.Section("ui").Key("email").String()
|
address = app.config.Section("ui").Key("email").String()
|
||||||
@ -1074,14 +1134,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
var address string
|
var address string
|
||||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||||
var ok bool
|
addr, ok := app.storage.emails[gc.GetString("jfId")]
|
||||||
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
||||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||||
respond(500, "Missing user email", gc)
|
respond(500, "Missing user email", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
address = addr.Addr
|
||||||
} else {
|
} else {
|
||||||
address = app.config.Section("ui").Key("email").String()
|
address = app.config.Section("ui").Key("email").String()
|
||||||
}
|
}
|
||||||
@ -1168,7 +1228,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
user.LastActive = jfUser.LastActivityDate.Unix()
|
user.LastActive = jfUser.LastActivityDate.Unix()
|
||||||
}
|
}
|
||||||
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.Addr
|
||||||
|
user.NotifyThroughEmail = email.Contact
|
||||||
}
|
}
|
||||||
expiry, ok := app.storage.users[jfUser.ID]
|
expiry, ok := app.storage.users[jfUser.ID]
|
||||||
if ok {
|
if ok {
|
||||||
@ -1178,6 +1239,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++
|
||||||
}
|
}
|
||||||
@ -1253,7 +1319,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
|||||||
for _, jfUser := range users {
|
for _, jfUser := range users {
|
||||||
id := jfUser.ID
|
id := jfUser.ID
|
||||||
if address, ok := req[id]; ok {
|
if address, ok := req[id]; ok {
|
||||||
app.storage.emails[id] = address
|
contact := true
|
||||||
|
if oldAddr, ok := app.storage.emails[id]; ok {
|
||||||
|
contact = oldAddr.Contact
|
||||||
|
}
|
||||||
|
app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
|
||||||
if ombiEnabled {
|
if ombiEnabled {
|
||||||
ombiUser, code, err := app.getOmbiUser(id)
|
ombiUser, code, err := app.getOmbiUser(id)
|
||||||
if code == 200 && err == nil {
|
if code == 200 && err == nil {
|
||||||
@ -2004,38 +2074,63 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
|||||||
|
|
||||||
// @Summary Sets whether to notify a user through telegram or not.
|
// @Summary Sets whether to notify a user through telegram or not.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Success 400 {object} boolResponse
|
// @Success 400 {object} boolResponse
|
||||||
// @Success 500 {object} boolResponse
|
// @Success 500 {object} boolResponse
|
||||||
// @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 SetContactMethodsDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
if req.ID == "" {
|
if req.ID == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
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)
|
||||||
|
}
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
|
msg := ""
|
||||||
respondBool(400, false, gc)
|
if !req.Discord {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||||
|
}
|
||||||
|
if email, ok := app.storage.emails[req.ID]; ok {
|
||||||
|
email.Contact = req.Email
|
||||||
|
app.storage.emails[req.ID] = email
|
||||||
|
if err := app.storage.storeEmails(); err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Failed to store emails: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := ""
|
||||||
|
if !req.Email {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
|
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
|
||||||
@ -2092,6 +2187,107 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
|||||||
respondBool(200, tokenIndex != -1, gc)
|
respondBool(200, tokenIndex != -1, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Param pin path string true "PIN code to check"
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Router /invite/{invCode}/discord/verified/{pin} [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin := gc.Param("pin")
|
||||||
|
_, ok := app.discord.verifiedTokens[pin]
|
||||||
|
respondBool(200, ok, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns a 10-minute, one-use Discord server invite
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} DiscordInviteDTO
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Router /invite/{invCode}/discord/invite [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||||
|
if app.discord.inviteChannelName == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
|
||||||
|
if invURL == "" {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} DiscordUsersDTO
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param username path string true "username to search."
|
||||||
|
// @Router /users/discord/{username} [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
|
||||||
|
name := gc.Param("username")
|
||||||
|
if name == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
users := app.discord.GetUsers(name)
|
||||||
|
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
|
||||||
|
for i, u := range users {
|
||||||
|
resp.Users[i] = DiscordUserDTO{
|
||||||
|
Name: u.User.Username + "#" + u.User.Discriminator,
|
||||||
|
ID: u.User.ID,
|
||||||
|
AvatarURL: u.User.AvatarURL("32"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gc.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
|
||||||
|
// @Router /users/discord [post]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||||
|
var req DiscordConnectUserDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.JellyfinID == "" || req.DiscordID == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, ok := app.discord.NewUser(req.DiscordID)
|
||||||
|
if !ok {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.storage.discord[req.JellyfinID] = user
|
||||||
|
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||||
|
app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Restarts the program. No response means success.
|
// @Summary Restarts the program. No response means success.
|
||||||
// @Router /restart [post]
|
// @Router /restart [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
|
32
config.go
@ -14,6 +14,7 @@ import (
|
|||||||
var emailEnabled = false
|
var emailEnabled = false
|
||||||
var messagesEnabled = false
|
var messagesEnabled = false
|
||||||
var telegramEnabled = false
|
var telegramEnabled = false
|
||||||
|
var discordEnabled = false
|
||||||
|
|
||||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||||
val := app.config.Section(sect).Key(key).MustString("")
|
val := app.config.Section(sect).Key(key).MustString("")
|
||||||
@ -42,7 +43,7 @@ func (app *appContext) loadConfig() error {
|
|||||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
|
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users"} {
|
||||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||||
}
|
}
|
||||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||||
@ -87,15 +88,17 @@ func (app *appContext) loadConfig() error {
|
|||||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||||
|
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
|
||||||
if !messagesEnabled {
|
if !messagesEnabled {
|
||||||
emailEnabled = false
|
emailEnabled = false
|
||||||
telegramEnabled = false
|
telegramEnabled = false
|
||||||
|
discordEnabled = false
|
||||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||||
emailEnabled = false
|
emailEnabled = false
|
||||||
} else {
|
} else {
|
||||||
emailEnabled = true
|
emailEnabled = true
|
||||||
}
|
}
|
||||||
if !emailEnabled && !telegramEnabled {
|
if !emailEnabled && !telegramEnabled && !discordEnabled {
|
||||||
messagesEnabled = false
|
messagesEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,3 +171,28 @@ func (app *appContext) migrateEmailConfig() {
|
|||||||
}
|
}
|
||||||
app.loadConfig()
|
app.loadConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *appContext) migrateEmailStorage() error {
|
||||||
|
var emails map[string]interface{}
|
||||||
|
err := loadJSON(app.storage.emails_path, &emails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newEmails := map[string]EmailAddress{}
|
||||||
|
for jfID, addr := range emails {
|
||||||
|
newEmails[jfID] = EmailAddress{
|
||||||
|
Addr: addr.(string),
|
||||||
|
Contact: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = storeJSON(app.storage.emails_path+".bak", emails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = storeJSON(app.storage.emails_path, newEmails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.info.Println("Migrated to new email format. A backup has also been made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -546,6 +546,89 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"discord": {
|
||||||
|
"order": [],
|
||||||
|
"meta": {
|
||||||
|
"name": "Discord",
|
||||||
|
"description": "Settings for Discord invites/signup/notifications"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"enabled": {
|
||||||
|
"name": "Enabled",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"name": "Require on sign-up",
|
||||||
|
"required": false,
|
||||||
|
"required_restart": true,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"description": "Require Discord connection on sign-up."
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "API Token",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Discord Bot API Token."
|
||||||
|
},
|
||||||
|
"start_command": {
|
||||||
|
"name": "Start command",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "!start",
|
||||||
|
"description": "Command to start the user verification process."
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"name": "Channel to monitor",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Only listen to commands in specified channel. Leave blank to monitor all."
|
||||||
|
},
|
||||||
|
"provide_invite": {
|
||||||
|
"name": "Provide server invite",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"description": "Generate a one-time discord server invite for the account creation form. Required Bot permission \"Create instant invite\", you may need to re-add the bot to your server after."
|
||||||
|
},
|
||||||
|
"invite_channel": {
|
||||||
|
"name": "Invite channel",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"depends_true": "provide_invite",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Channel to invite new users to."
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "Language",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
["en-us", "English (US)"]
|
||||||
|
],
|
||||||
|
"value": "en-us",
|
||||||
|
"description": "Default Discord message language. Visit weblate if you'd like to translate."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"order": [],
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
@ -565,6 +648,7 @@
|
|||||||
"name": "Require on sign-up",
|
"name": "Require on sign-up",
|
||||||
"required": false,
|
"required": false,
|
||||||
"required_restart": true,
|
"required_restart": true,
|
||||||
|
"depends_true": "enabled",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": false,
|
"value": false,
|
||||||
"description": "Require telegram connection on sign-up."
|
"description": "Require telegram connection on sign-up."
|
||||||
@ -1140,6 +1224,14 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "",
|
"value": "",
|
||||||
"description": "Stores telegram user IDs and language preferences."
|
"description": "Stores telegram user IDs and language preferences."
|
||||||
|
},
|
||||||
|
"discord_users": {
|
||||||
|
"name": "Discord users",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Stores discord user IDs and language preferences."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
44
css/base.css
@ -30,12 +30,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 1000px) {
|
||||||
:root {
|
:root {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.table-responsive table {
|
.table-responsive table {
|
||||||
min-width: 660px;
|
min-width: 800px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-100 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-block {
|
.inline-block {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@ -172,7 +176,7 @@ div.card:contains(section.banner.footer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.sm,
|
p.sm,
|
||||||
span.sm {
|
span.sm:not(.heading) {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,6 +428,7 @@ p.top {
|
|||||||
|
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-box {
|
#notification-box {
|
||||||
@ -438,6 +443,10 @@ p.top {
|
|||||||
margin-bottom: -0.5rem;
|
margin-bottom: -0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-display.lg {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
white-space: pre-wrap; /* css-3 */
|
white-space: pre-wrap; /* css-3 */
|
||||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||||
@ -483,3 +492,32 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
|||||||
max-width: 15rem;
|
max-width: 15rem;
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.img-circle {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.img-circle.lg {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.shield.img-circle {
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.img-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td.sm {
|
||||||
|
padding-top: 0.1rem;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-inline {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
404
discord.go
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dg "github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscordDaemon struct {
|
||||||
|
Stopped bool
|
||||||
|
ShutdownChannel chan string
|
||||||
|
bot *dg.Session
|
||||||
|
username string
|
||||||
|
tokens []string
|
||||||
|
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
|
||||||
|
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||||
|
guildID string
|
||||||
|
serverChannelName, serverName string
|
||||||
|
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
||||||
|
app *appContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||||
|
token := app.config.Section("discord").Key("token").String()
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("token was blank")
|
||||||
|
}
|
||||||
|
bot, err := dg.New("Bot " + token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dd := &DiscordDaemon{
|
||||||
|
Stopped: false,
|
||||||
|
ShutdownChannel: make(chan string),
|
||||||
|
bot: bot,
|
||||||
|
tokens: []string{},
|
||||||
|
verifiedTokens: map[string]DiscordUser{},
|
||||||
|
users: map[string]DiscordUser{},
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
for _, user := range app.storage.discord {
|
||||||
|
dd.users[user.ID] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
return dd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||||
|
func (d *DiscordDaemon) NewAuthToken() string {
|
||||||
|
pin := genAuthToken()
|
||||||
|
d.tokens = append(d.tokens, pin)
|
||||||
|
return pin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) NewUnknownUser(channelID, userID, discrim, username string) DiscordUser {
|
||||||
|
user := DiscordUser{
|
||||||
|
ChannelID: channelID,
|
||||||
|
ID: userID,
|
||||||
|
Username: username,
|
||||||
|
Discriminator: discrim,
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) DiscordUser {
|
||||||
|
if user, ok := d.users[userID]; ok {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return d.NewUnknownUser(channelID, userID, discrim, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) run() {
|
||||||
|
d.bot.AddHandler(d.messageHandler)
|
||||||
|
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
|
||||||
|
if err := d.bot.Open(); err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Sometimes bot.State isn't populated quick enough
|
||||||
|
for d.bot.State == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d.username = d.bot.State.User.Username
|
||||||
|
// Choose the last guild (server), for now we don't really support multiple anyway
|
||||||
|
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
|
||||||
|
guild, err := d.bot.Guild(d.guildID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||||
|
}
|
||||||
|
d.serverChannelName = guild.Name
|
||||||
|
d.serverName = guild.Name
|
||||||
|
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||||
|
d.channelName = channel
|
||||||
|
d.serverChannelName += "/" + channel
|
||||||
|
}
|
||||||
|
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||||
|
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
||||||
|
d.inviteChannelName = invChannel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer d.bot.Close()
|
||||||
|
<-d.ShutdownChannel
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon.
|
||||||
|
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
||||||
|
var inv *dg.Invite
|
||||||
|
var err error
|
||||||
|
if d.inviteChannelName == "" {
|
||||||
|
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.inviteChannelID == "" {
|
||||||
|
channels, err := d.bot.GuildChannels(d.guildID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, channel := range channels {
|
||||||
|
// channel, err := d.bot.Channel(ch.ID)
|
||||||
|
// if err != nil {
|
||||||
|
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
if channel.Name == d.inviteChannelName {
|
||||||
|
d.inviteChannelID = channel.ID
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// channel, err := d.bot.Channel(d.inviteChannelID)
|
||||||
|
// if err != nil {
|
||||||
|
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
||||||
|
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
||||||
|
// Channel: channel,
|
||||||
|
// Inviter: d.bot.State.User,
|
||||||
|
MaxAge: ageSeconds,
|
||||||
|
MaxUses: maxUses,
|
||||||
|
Temporary: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inviteURL = "https://discord.gg/" + inv.Code
|
||||||
|
guild, err := d.bot.Guild(d.guildID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iconURL = guild.IconURL()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the user(s) roughly corresponding to the username (if they are in the guild).
|
||||||
|
// if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned.
|
||||||
|
func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
||||||
|
members, err := d.bot.GuildMembers(
|
||||||
|
d.guildID,
|
||||||
|
"",
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hasDiscriminator := strings.Contains(username, "#")
|
||||||
|
var users []*dg.Member
|
||||||
|
for _, member := range members {
|
||||||
|
if hasDiscriminator {
|
||||||
|
if member.User.Username+"#"+member.User.Discriminator == username {
|
||||||
|
return []*dg.Member{member}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(member.User.Username, username) {
|
||||||
|
users = append(users, member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
||||||
|
u, err := d.bot.User(ID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to get user: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.ID = ID
|
||||||
|
user.Username = u.Username
|
||||||
|
user.Contact = true
|
||||||
|
user.Discriminator = u.Discriminator
|
||||||
|
channel, err := d.bot.UserChannelCreate(ID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.ChannelID = channel.ID
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) Shutdown() {
|
||||||
|
d.Stopped = true
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
<-d.ShutdownChannel
|
||||||
|
close(d.ShutdownChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
||||||
|
if m.GuildID != "" && d.channelName != "" {
|
||||||
|
if d.channelID == "" {
|
||||||
|
channel, err := s.Channel(m.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||||
|
d.channelName = ""
|
||||||
|
}
|
||||||
|
if channel.Name == d.channelName {
|
||||||
|
d.channelID = channel.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.channelID != m.ChannelID {
|
||||||
|
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Author.ID == s.State.User.ID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sects := strings.Split(m.Content, " ")
|
||||||
|
if len(sects) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lang := d.app.storage.lang.chosenTelegramLang
|
||||||
|
if user, ok := d.users[m.Author.ID]; ok {
|
||||||
|
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
|
||||||
|
lang = user.Lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch msg := sects[0]; msg {
|
||||||
|
case d.app.config.Section("discord").Key("start_command").MustString("!start"):
|
||||||
|
d.commandStart(s, m, lang)
|
||||||
|
case "!lang":
|
||||||
|
d.commandLang(s, m, sects, lang)
|
||||||
|
default:
|
||||||
|
d.commandPIN(s, m, sects, lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang string) {
|
||||||
|
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
|
||||||
|
d.users[m.Author.ID] = user
|
||||||
|
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||||
|
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
|
||||||
|
_, err = s.ChannelMessageSend(channel.ID, content)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||||
|
if len(sects) == 1 {
|
||||||
|
list := "!lang <lang>\n"
|
||||||
|
for code := range d.app.storage.lang.Telegram {
|
||||||
|
list += fmt.Sprintf("%s: %s\n", code, d.app.storage.lang.Telegram[code].Meta.Name)
|
||||||
|
}
|
||||||
|
_, err := s.ChannelMessageSendReply(
|
||||||
|
m.ChannelID,
|
||||||
|
list,
|
||||||
|
m.Reference(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
||||||
|
var user DiscordUser
|
||||||
|
for jfID, user := range d.app.storage.discord {
|
||||||
|
if user.ID == m.Author.ID {
|
||||||
|
user.Lang = sects[1]
|
||||||
|
d.app.storage.discord[jfID] = user
|
||||||
|
if err := d.app.storage.storeDiscordUsers(); err != nil {
|
||||||
|
d.app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.users[m.Author.ID] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := s.ChannelMessageSend(
|
||||||
|
m.ChannelID,
|
||||||
|
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
}
|
||||||
|
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) SendDM(message *Message, userID ...string) error {
|
||||||
|
channels := make([]string, len(userID))
|
||||||
|
for i, id := range userID {
|
||||||
|
channel, err := d.bot.UserChannelCreate(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
channels[i] = channel.ID
|
||||||
|
}
|
||||||
|
return d.Send(message, channels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
||||||
|
msg := ""
|
||||||
|
var embeds []*dg.MessageEmbed
|
||||||
|
if message.Markdown != "" {
|
||||||
|
msg, embeds = StripAltText(message.Markdown, true)
|
||||||
|
} else {
|
||||||
|
msg = message.Text
|
||||||
|
}
|
||||||
|
for _, id := range channelID {
|
||||||
|
var err error
|
||||||
|
if len(embeds) != 0 {
|
||||||
|
_, err = d.bot.ChannelMessageSendComplex(
|
||||||
|
id,
|
||||||
|
&dg.MessageSend{
|
||||||
|
Content: msg,
|
||||||
|
Embed: embeds[0],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := 1; i < len(embeds); i++ {
|
||||||
|
_, err := d.bot.ChannelMessageSendEmbed(id, embeds[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := d.bot.ChannelMessageSend(
|
||||||
|
id,
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
26
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 {
|
||||||
err = app.email.send(email, address.(string))
|
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 && address.Contact && emailEnabled {
|
||||||
|
err = app.email.send(email, address.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -818,11 +831,14 @@ 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
|
||||||
}
|
}
|
||||||
if addr, ok := app.storage.emails[jfID]; ok {
|
if addr, ok := app.storage.emails[jfID]; ok {
|
||||||
return addr.(string)
|
return addr.Addr
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
1
go.mod
@ -11,6 +11,7 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
|
|||||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bwmarrin/discordgo v0.23.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a // indirect
|
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a // indirect
|
||||||
|
5
go.sum
@ -11,6 +11,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
|
|||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
|
||||||
|
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||||
@ -148,6 +150,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
|
|||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
|
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
|
||||||
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||||
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
|
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
|
||||||
@ -265,6 +269,7 @@ github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
|
|||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
|
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
|
||||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -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 }});
|
||||||
@ -327,6 +328,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .discord_enabled }}
|
||||||
|
<div id="modal-discord" class="modal">
|
||||||
|
<div class="modal-content card">
|
||||||
|
<span class="heading mb-1"><span id="discord-header"></span><span class="modal-close">×</span></span>
|
||||||
|
<p class="content mb-1" id="discord-description"></p>
|
||||||
|
<div class="row">
|
||||||
|
<input type="search" class="col sm field ~neutral !normal input" id="discord-search" placeholder="user#1234">
|
||||||
|
</div>
|
||||||
|
<table class="table"><tbody id="discord-list"></tbody></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<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">
|
||||||
@ -488,7 +501,14 @@
|
|||||||
<div id="create-send-to-container">
|
<div id="create-send-to-container">
|
||||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
||||||
<div class="flex-expand mb-1 mt-half">
|
<div class="flex-expand mb-1 mt-half">
|
||||||
|
{{ if .discord_enabled }}
|
||||||
|
<input type="text" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com | user#1234">
|
||||||
|
<span id="create-send-to-search" class="button ~neutral !normal mr-1">
|
||||||
|
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
|
||||||
|
</span>
|
||||||
|
{{ else }}
|
||||||
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
||||||
|
{{ end }}
|
||||||
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
||||||
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||||
</label>
|
</label>
|
||||||
@ -525,6 +545,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,11 @@
|
|||||||
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 }}";
|
||||||
|
window.discordInviteLink = {{ .discordInviteLink }};
|
||||||
|
window.discordServerName = "{{ .discordServerName }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="js/form.js" type="module"></script>
|
<script src="js/form.js" type="module"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -37,6 +37,17 @@
|
|||||||
</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>
|
||||||
|
<a id="discord-invite"></a>
|
||||||
|
<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 +80,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>
|
||||||
|
BIN
images/discord/1.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
images/discord/2.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
images/discord/3.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
images/discord/4.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
images/discord/5.jpg
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
images/discord/6.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
images/discord/7.jpg
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
images/discord/8.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
@ -20,6 +20,8 @@
|
|||||||
"create": "Create",
|
"create": "Create",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"add": "Add",
|
||||||
|
"select": "Select",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
@ -94,7 +96,9 @@
|
|||||||
"notifyEvent": "Notify on:",
|
"notifyEvent": "Notify on:",
|
||||||
"notifyInviteExpiry": "On expiry",
|
"notifyInviteExpiry": "On expiry",
|
||||||
"notifyUserCreation": "On user creation",
|
"notifyUserCreation": "On user creation",
|
||||||
"sendPIN": "Ask the user to send the PIN below to the bot."
|
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
||||||
|
"searchDiscordUser": "Start typing the Discord username to find the user.",
|
||||||
|
"findDiscordUser": "Find Discord user"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"changedEmailAddress": "Changed email address of {n}.",
|
"changedEmailAddress": "Changed email address of {n}.",
|
||||||
@ -107,6 +111,7 @@
|
|||||||
"updateApplied": "Update applied, please restart.",
|
"updateApplied": "Update applied, please restart.",
|
||||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||||
"telegramVerified": "Telegram account verified.",
|
"telegramVerified": "Telegram account verified.",
|
||||||
|
"accountConnected": "Account connected.",
|
||||||
"errorConnection": "Couldn't connect to jfa-go.",
|
"errorConnection": "Couldn't connect to jfa-go.",
|
||||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||||
|
@ -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": {
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
|
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
|
||||||
"invalidPIN": "That PIN was invalid, try again.",
|
"invalidPIN": "That PIN was invalid, try again.",
|
||||||
"pinSuccess": "Success! You can now return to the sign-up page.",
|
"pinSuccess": "Success! You can now return to the sign-up page.",
|
||||||
"languageMessage": "Note: See available languages with /lang, and set language with /lang <language code>."
|
"languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
159
main.go
@ -16,7 +16,6 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -96,6 +95,7 @@ type appContext struct {
|
|||||||
validator Validator
|
validator Validator
|
||||||
email *Emailer
|
email *Emailer
|
||||||
telegram *TelegramDaemon
|
telegram *TelegramDaemon
|
||||||
|
discord *DiscordDaemon
|
||||||
info, debug, err logger.Logger
|
info, debug, err logger.Logger
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
@ -320,6 +320,10 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.storage.emails_path = app.config.Section("files").Key("emails").String()
|
app.storage.emails_path = app.config.Section("files").Key("emails").String()
|
||||||
if err := app.storage.loadEmails(); err != nil {
|
if err := app.storage.loadEmails(); err != nil {
|
||||||
app.err.Printf("Failed to load Emails: %v", err)
|
app.err.Printf("Failed to load Emails: %v", err)
|
||||||
|
err := app.migrateEmailStorage()
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to migrate Email storage: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||||
if err := app.storage.loadPolicy(); err != nil {
|
if err := app.storage.loadPolicy(); err != nil {
|
||||||
@ -341,6 +345,10 @@ func start(asDaemon, firstCall bool) {
|
|||||||
if err := app.storage.loadTelegramUsers(); err != nil {
|
if err := app.storage.loadTelegramUsers(); err != nil {
|
||||||
app.err.Printf("Failed to load Telegram users: %v", err)
|
app.err.Printf("Failed to load Telegram users: %v", err)
|
||||||
}
|
}
|
||||||
|
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
|
||||||
|
if err := app.storage.loadDiscordUsers(); err != nil {
|
||||||
|
app.err.Printf("Failed to load Discord users: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||||
app.storage.loadProfiles()
|
app.storage.loadProfiles()
|
||||||
@ -429,76 +437,76 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
|
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
|
||||||
}
|
}
|
||||||
app.info.Printf("Authenticated with %s", server)
|
app.info.Printf("Authenticated with %s", server)
|
||||||
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
|
// /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
|
||||||
This checks if the version is equal or higher. */
|
// This checks if the version is equal or higher. */
|
||||||
checkVersion := func(version string) int {
|
// checkVersion := func(version string) int {
|
||||||
numberStrings := strings.Split(version, ".")
|
// numberStrings := strings.Split(version, ".")
|
||||||
n := 0
|
// n := 0
|
||||||
for _, s := range numberStrings {
|
// for _, s := range numberStrings {
|
||||||
num, err := strconv.Atoi(s)
|
// num, err := strconv.Atoi(s)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
n += num
|
// n += num
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return n
|
// return n
|
||||||
}
|
// }
|
||||||
if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
|
// if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
|
||||||
// Get users to check if server uses hyphenated userIDs
|
// // Get users to check if server uses hyphenated userIDs
|
||||||
app.jf.GetUsers(false)
|
// app.jf.GetUsers(false)
|
||||||
|
|
||||||
noHyphens := true
|
// noHyphens := true
|
||||||
for id := range app.storage.emails {
|
// for id := range app.storage.emails {
|
||||||
if strings.Contains(id, "-") {
|
// if strings.Contains(id, "-") {
|
||||||
noHyphens = false
|
// noHyphens = false
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if noHyphens == app.jf.Hyphens {
|
// if noHyphens == app.jf.Hyphens {
|
||||||
var newEmails map[string]interface{}
|
// var newEmails map[string]interface{}
|
||||||
var newUsers map[string]time.Time
|
// var newUsers map[string]time.Time
|
||||||
var status, status2 int
|
// var status, status2 int
|
||||||
var err, err2 error
|
// var err, err2 error
|
||||||
if app.jf.Hyphens {
|
// if app.jf.Hyphens {
|
||||||
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
|
// app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
|
||||||
time.Sleep(time.Second * time.Duration(3))
|
// time.Sleep(time.Second * time.Duration(3))
|
||||||
newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
|
// newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
|
||||||
newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
|
// newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
|
||||||
} else {
|
// } else {
|
||||||
app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
|
// app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
|
||||||
time.Sleep(time.Second * time.Duration(3))
|
// time.Sleep(time.Second * time.Duration(3))
|
||||||
newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
|
// newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
|
||||||
newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
|
// newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
|
||||||
}
|
// }
|
||||||
if status != 200 || err != nil {
|
// if status != 200 || err != nil {
|
||||||
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||||
app.err.Fatalf("Couldn't upgrade emails.json")
|
// app.err.Fatalf("Couldn't upgrade emails.json")
|
||||||
}
|
// }
|
||||||
if status2 != 200 || err2 != nil {
|
// if status2 != 200 || err2 != nil {
|
||||||
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||||
app.err.Fatalf("Couldn't upgrade users.json")
|
// app.err.Fatalf("Couldn't upgrade users.json")
|
||||||
}
|
// }
|
||||||
emailBakFile := app.storage.emails_path + ".bak"
|
// emailBakFile := app.storage.emails_path + ".bak"
|
||||||
usersBakFile := app.storage.users_path + ".bak"
|
// usersBakFile := app.storage.users_path + ".bak"
|
||||||
err = storeJSON(emailBakFile, app.storage.emails)
|
// err = storeJSON(emailBakFile, app.storage.emails)
|
||||||
err2 = storeJSON(usersBakFile, app.storage.users)
|
// err2 = storeJSON(usersBakFile, app.storage.users)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
app.err.Fatalf("couldn't store emails.json backup: %v", err)
|
// app.err.Fatalf("couldn't store emails.json backup: %v", err)
|
||||||
}
|
// }
|
||||||
if err2 != nil {
|
// if err2 != nil {
|
||||||
app.err.Fatalf("couldn't store users.json backup: %v", err)
|
// app.err.Fatalf("couldn't store users.json backup: %v", err)
|
||||||
}
|
// }
|
||||||
app.storage.emails = newEmails
|
// app.storage.emails = newEmails
|
||||||
app.storage.users = newUsers
|
// app.storage.users = newUsers
|
||||||
err = app.storage.storeEmails()
|
// err = app.storage.storeEmails()
|
||||||
err2 = app.storage.storeUsers()
|
// err2 = app.storage.storeUsers()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
app.err.Fatalf("couldn't store emails.json: %v", err)
|
// app.err.Fatalf("couldn't store emails.json: %v", err)
|
||||||
}
|
// }
|
||||||
if err2 != nil {
|
// if err2 != nil {
|
||||||
app.err.Fatalf("couldn't store users.json: %v", err)
|
// app.err.Fatalf("couldn't store users.json: %v", err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Auth (manual user/pass or jellyfin)
|
// Auth (manual user/pass or jellyfin)
|
||||||
app.jellyfinLogin = true
|
app.jellyfinLogin = true
|
||||||
@ -562,11 +570,22 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.telegram, err = newTelegramDaemon(app)
|
app.telegram, err = newTelegramDaemon(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
||||||
|
telegramEnabled = false
|
||||||
} else {
|
} else {
|
||||||
go app.telegram.run()
|
go app.telegram.run()
|
||||||
defer app.telegram.Shutdown()
|
defer app.telegram.Shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if discordEnabled {
|
||||||
|
app.discord, err = newDiscordDaemon(app)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
||||||
|
discordEnabled = false
|
||||||
|
} else {
|
||||||
|
go app.discord.run()
|
||||||
|
defer app.discord.Shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debugMode = false
|
debugMode = false
|
||||||
address = "0.0.0.0:8056"
|
address = "0.0.0.0:8056"
|
||||||
|
36
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 {
|
||||||
@ -48,7 +50,7 @@ type generateInviteDTO struct {
|
|||||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||||
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
|
||||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||||
@ -98,7 +100,7 @@ type inviteDTO struct {
|
|||||||
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
|
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
|
||||||
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
||||||
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
||||||
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable)
|
||||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||||
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
||||||
@ -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)
|
||||||
|
NotifyThroughEmail bool `json:"notify_email"`
|
||||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
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
|
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.
|
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.
|
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||||
Telegram string `json:"telegram"` // Telegram username (if known)
|
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 {
|
||||||
@ -249,7 +255,29 @@ type telegramSetDTO struct {
|
|||||||
ID string `json:"id"` // Jellyfin ID of user.
|
ID string `json:"id"` // Jellyfin ID of user.
|
||||||
}
|
}
|
||||||
|
|
||||||
type telegramNotifyDTO struct {
|
type SetContactMethodsDTO 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordUserDTO struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordUsersDTO struct {
|
||||||
|
Users []DiscordUserDTO `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordConnectUserDTO struct {
|
||||||
|
JellyfinID string `json:"jf_id"`
|
||||||
|
DiscordID string `json:"discord_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordInviteDTO struct {
|
||||||
|
InviteURL string `json:"invite"`
|
||||||
|
IconURL string `json:"icon"`
|
||||||
}
|
}
|
||||||
|
14
router.go
@ -121,6 +121,12 @@ 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 app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||||
|
router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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"))
|
||||||
@ -158,11 +164,15 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
api.GET(p+"/config", app.GetConfig)
|
api.GET(p+"/config", app.GetConfig)
|
||||||
api.POST(p+"/config", app.ModifyConfig)
|
api.POST(p+"/config", app.ModifyConfig)
|
||||||
api.POST(p+"/restart", app.restart)
|
api.POST(p+"/restart", app.restart)
|
||||||
if telegramEnabled {
|
if telegramEnabled || discordEnabled {
|
||||||
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 discordEnabled {
|
||||||
|
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
|
||||||
|
api.POST(p+"/users/discord", app.DiscordConnect)
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
195
storage.go
@ -16,13 +16,15 @@ import (
|
|||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
timePattern string
|
timePattern string
|
||||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
|
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path string
|
||||||
users map[string]time.Time
|
users map[string]time.Time
|
||||||
invites Invites
|
invites Invites
|
||||||
profiles map[string]Profile
|
profiles map[string]Profile
|
||||||
defaultProfile string
|
defaultProfile string
|
||||||
emails, displayprefs, ombi_template map[string]interface{}
|
displayprefs, ombi_template map[string]interface{}
|
||||||
|
emails map[string]EmailAddress
|
||||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||||
|
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||||
customEmails customEmails
|
customEmails customEmails
|
||||||
policy mediabrowser.Policy
|
policy mediabrowser.Policy
|
||||||
configuration mediabrowser.Configuration
|
configuration mediabrowser.Configuration
|
||||||
@ -37,6 +39,20 @@ type TelegramUser struct {
|
|||||||
Contact bool // Whether to contact through telegram or not
|
Contact bool // Whether to contact through telegram or not
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiscordUser struct {
|
||||||
|
ChannelID string
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Discriminator string
|
||||||
|
Lang string
|
||||||
|
Contact bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailAddress struct {
|
||||||
|
Addr string
|
||||||
|
Contact bool
|
||||||
|
}
|
||||||
|
|
||||||
type customEmails struct {
|
type customEmails struct {
|
||||||
UserCreated customEmail `json:"userCreated"`
|
UserCreated customEmail `json:"userCreated"`
|
||||||
InviteExpiry customEmail `json:"inviteExpiry"`
|
InviteExpiry customEmail `json:"inviteExpiry"`
|
||||||
@ -79,7 +95,7 @@ type Invite struct {
|
|||||||
UserDays int `json:"user-days,omitempty"`
|
UserDays int `json:"user-days,omitempty"`
|
||||||
UserHours int `json:"user-hours,omitempty"`
|
UserHours int `json:"user-hours,omitempty"`
|
||||||
UserMinutes int `json:"user-minutes,omitempty"`
|
UserMinutes int `json:"user-minutes,omitempty"`
|
||||||
Email string `json:"email"`
|
SendTo string `json:"email"`
|
||||||
// Used to be stored as formatted time, now as Unix.
|
// Used to be stored as formatted time, now as Unix.
|
||||||
UsedBy [][]string `json:"used-by"`
|
UsedBy [][]string `json:"used-by"`
|
||||||
Notify map[string]map[string]bool `json:"notify"`
|
Notify map[string]map[string]bool `json:"notify"`
|
||||||
@ -106,6 +122,7 @@ type Lang struct {
|
|||||||
Common commonLangs
|
Common commonLangs
|
||||||
SetupPath string
|
SetupPath string
|
||||||
Setup setupLangs
|
Setup setupLangs
|
||||||
|
// Telegram translations are also used for Discord bots (and likely future ones).
|
||||||
chosenTelegramLang string
|
chosenTelegramLang string
|
||||||
TelegramPath string
|
TelegramPath string
|
||||||
Telegram telegramLangs
|
Telegram telegramLangs
|
||||||
@ -765,6 +782,14 @@ func (st *Storage) storeTelegramUsers() error {
|
|||||||
return storeJSON(st.telegram_path, st.telegram)
|
return storeJSON(st.telegram_path, st.telegram)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (st *Storage) loadDiscordUsers() error {
|
||||||
|
return loadJSON(st.discord_path, &st.discord)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *Storage) storeDiscordUsers() error {
|
||||||
|
return storeJSON(st.discord_path, st.discord)
|
||||||
|
}
|
||||||
|
|
||||||
func (st *Storage) loadCustomEmails() error {
|
func (st *Storage) loadCustomEmails() error {
|
||||||
return loadJSON(st.customEmails_path, &st.customEmails)
|
return loadJSON(st.customEmails_path, &st.customEmails)
|
||||||
}
|
}
|
||||||
@ -884,85 +909,85 @@ func storeJSON(path string, obj interface{}) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
|
// // One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
|
||||||
|
//
|
||||||
func hyphenate(userID string) string {
|
// func hyphenate(userID string) string {
|
||||||
if userID[8] == '-' {
|
// if userID[8] == '-' {
|
||||||
return userID
|
// return userID
|
||||||
}
|
// }
|
||||||
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
|
// return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
// func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||||
jfUsers, status, err := app.jf.GetUsers(false)
|
// jfUsers, status, err := app.jf.GetUsers(false)
|
||||||
if status != 200 || err != nil {
|
// if status != 200 || err != nil {
|
||||||
return nil, status, err
|
// return nil, status, err
|
||||||
}
|
// }
|
||||||
newEmails := map[string]interface{}{}
|
// newEmails := map[string]interface{}{}
|
||||||
for _, user := range jfUsers {
|
// for _, user := range jfUsers {
|
||||||
unHyphenated := user.ID
|
// unHyphenated := user.ID
|
||||||
hyphenated := hyphenate(unHyphenated)
|
// hyphenated := hyphenate(unHyphenated)
|
||||||
val, ok := old[hyphenated]
|
// val, ok := old[hyphenated]
|
||||||
if ok {
|
// if ok {
|
||||||
newEmails[unHyphenated] = val
|
// newEmails[unHyphenated] = val
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return newEmails, status, err
|
// return newEmails, status, err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
// func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||||
jfUsers, status, err := app.jf.GetUsers(false)
|
// jfUsers, status, err := app.jf.GetUsers(false)
|
||||||
if status != 200 || err != nil {
|
// if status != 200 || err != nil {
|
||||||
return nil, status, err
|
// return nil, status, err
|
||||||
}
|
// }
|
||||||
newEmails := map[string]interface{}{}
|
// newEmails := map[string]interface{}{}
|
||||||
for _, user := range jfUsers {
|
// for _, user := range jfUsers {
|
||||||
unstripped := user.ID
|
// unstripped := user.ID
|
||||||
stripped := strings.ReplaceAll(unstripped, "-", "")
|
// stripped := strings.ReplaceAll(unstripped, "-", "")
|
||||||
val, ok := old[stripped]
|
// val, ok := old[stripped]
|
||||||
if ok {
|
// if ok {
|
||||||
newEmails[unstripped] = val
|
// newEmails[unstripped] = val
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return newEmails, status, err
|
// return newEmails, status, err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
// func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||||
return app.hyphenateStorage(old)
|
// return app.hyphenateStorage(old)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
// func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||||
return app.deHyphenateStorage(old)
|
// return app.deHyphenateStorage(old)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
// func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
||||||
asInterface := map[string]interface{}{}
|
// asInterface := map[string]interface{}{}
|
||||||
for k, v := range old {
|
// for k, v := range old {
|
||||||
asInterface[k] = v
|
// asInterface[k] = v
|
||||||
}
|
// }
|
||||||
fixed, status, err := app.hyphenateStorage(asInterface)
|
// fixed, status, err := app.hyphenateStorage(asInterface)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, status, err
|
// return nil, status, err
|
||||||
}
|
// }
|
||||||
out := map[string]time.Time{}
|
// out := map[string]time.Time{}
|
||||||
for k, v := range fixed {
|
// for k, v := range fixed {
|
||||||
out[k] = v.(time.Time)
|
// out[k] = v.(time.Time)
|
||||||
}
|
// }
|
||||||
return out, status, err
|
// return out, status, err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
// func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
||||||
asInterface := map[string]interface{}{}
|
// asInterface := map[string]interface{}{}
|
||||||
for k, v := range old {
|
// for k, v := range old {
|
||||||
asInterface[k] = v
|
// asInterface[k] = v
|
||||||
}
|
// }
|
||||||
fixed, status, err := app.deHyphenateStorage(asInterface)
|
// fixed, status, err := app.deHyphenateStorage(asInterface)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, status, err
|
// return nil, status, err
|
||||||
}
|
// }
|
||||||
out := map[string]time.Time{}
|
// out := map[string]time.Time{}
|
||||||
for k, v := range fixed {
|
// for k, v := range fixed {
|
||||||
out[k] = v.(time.Time)
|
// out[k] = v.(time.Time)
|
||||||
}
|
// }
|
||||||
return out, status, err
|
// return out, status, err
|
||||||
}
|
// }
|
||||||
|
46
stripmd.go
@ -3,22 +3,57 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
dg "github.com/bwmarrin/discordgo"
|
||||||
stripmd "github.com/writeas/go-strip-markdown"
|
stripmd "github.com/writeas/go-strip-markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Alt, URL string
|
||||||
|
}
|
||||||
|
|
||||||
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
|
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
|
||||||
// Currently uses the deepest alt text when links/images are nested.
|
// Currently uses the deepest alt text when links/images are nested.
|
||||||
func StripAltText(md string) string {
|
// If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
|
||||||
|
func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
|
||||||
altTextStart := -1 // Start of alt text (between '[' & ']')
|
altTextStart := -1 // Start of alt text (between '[' & ']')
|
||||||
URLStart := -1 // Start of url (between '(' & ')')
|
URLStart := -1 // Start of url (between '(' & ')')
|
||||||
URLEnd := -1
|
URLEnd := -1
|
||||||
previousURLEnd := -2
|
previousURLEnd := -2
|
||||||
out := ""
|
out := ""
|
||||||
|
embeds := []*dg.MessageEmbed{}
|
||||||
for i := range md {
|
for i := range md {
|
||||||
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
|
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
|
||||||
URLEnd = i - 1
|
URLEnd = i - 1
|
||||||
out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+1]
|
out += md[previousURLEnd+2 : altTextStart-1]
|
||||||
|
if links {
|
||||||
|
embed := &dg.MessageEmbed{
|
||||||
|
Type: dg.EmbedTypeLink,
|
||||||
|
Title: md[altTextStart : URLStart-2],
|
||||||
|
}
|
||||||
|
if md[altTextStart-1] == '!' {
|
||||||
|
embed.Title = md[altTextStart+1 : URLStart-2]
|
||||||
|
embed.Type = dg.EmbedTypeImage
|
||||||
|
embed.Image = &dg.MessageEmbedImage{
|
||||||
|
URL: md[URLStart : URLEnd+1],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
embed.URL = md[URLStart : URLEnd+1]
|
||||||
|
}
|
||||||
|
embeds = append(embeds, embed)
|
||||||
|
} else {
|
||||||
|
out += md[URLStart : URLEnd+1]
|
||||||
|
}
|
||||||
previousURLEnd = URLEnd
|
previousURLEnd = URLEnd
|
||||||
|
// Removing links often leaves a load of extra newlines which look weird, this removes them.
|
||||||
|
if links {
|
||||||
|
next := 2
|
||||||
|
for md[URLEnd+next] == '\n' {
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
if next >= 3 {
|
||||||
|
previousURLEnd += next - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
altTextStart, URLStart, URLEnd = -1, -1, -1
|
altTextStart, URLStart, URLEnd = -1, -1, -1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -36,11 +71,12 @@ func StripAltText(md string) string {
|
|||||||
out += md[previousURLEnd+2:]
|
out += md[previousURLEnd+2:]
|
||||||
}
|
}
|
||||||
if out == "" {
|
if out == "" {
|
||||||
return md
|
return md, embeds
|
||||||
}
|
}
|
||||||
return out
|
return out, embeds
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripMarkdown(md string) string {
|
func stripMarkdown(md string) string {
|
||||||
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "</p>"), "<p>")
|
stripped, _ := StripAltText(md, false)
|
||||||
|
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "</p>"), "<p>")
|
||||||
}
|
}
|
||||||
|
34
telegram.go
@ -9,7 +9,7 @@ import (
|
|||||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VerifiedToken struct {
|
type TelegramVerifiedToken struct {
|
||||||
Token string
|
Token string
|
||||||
ChatID int64
|
ChatID int64
|
||||||
Username string
|
Username string
|
||||||
@ -21,7 +21,7 @@ type TelegramDaemon struct {
|
|||||||
bot *tg.BotAPI
|
bot *tg.BotAPI
|
||||||
username string
|
username string
|
||||||
tokens []string
|
tokens []string
|
||||||
verifiedTokens []VerifiedToken
|
verifiedTokens []TelegramVerifiedToken
|
||||||
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
||||||
link string
|
link string
|
||||||
app *appContext
|
app *appContext
|
||||||
@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
td := &TelegramDaemon{
|
td := &TelegramDaemon{
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
ShutdownChannel: make(chan string),
|
||||||
bot: bot,
|
bot: bot,
|
||||||
username: bot.Self.UserName,
|
username: bot.Self.UserName,
|
||||||
tokens: []string{},
|
tokens: []string{},
|
||||||
verifiedTokens: []VerifiedToken{},
|
verifiedTokens: []TelegramVerifiedToken{},
|
||||||
languages: map[int64]string{},
|
languages: map[int64]string{},
|
||||||
link: "https://t.me/" + bot.Self.UserName,
|
link: "https://t.me/" + bot.Self.UserName,
|
||||||
app: app,
|
app: app,
|
||||||
@ -55,10 +54,7 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
|||||||
return td, nil
|
return td, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
func genAuthToken() string {
|
||||||
|
|
||||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
|
||||||
func (t *TelegramDaemon) NewAuthToken() string {
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
pin := make([]rune, 8)
|
pin := make([]rune, 8)
|
||||||
for i := range pin {
|
for i := range pin {
|
||||||
@ -68,10 +64,18 @@ func (t *TelegramDaemon) NewAuthToken() string {
|
|||||||
pin[i] = runes[rand.Intn(len(runes))]
|
pin[i] = runes[rand.Intn(len(runes))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.tokens = append(t.tokens, string(pin))
|
|
||||||
return string(pin)
|
return string(pin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
|
|
||||||
|
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||||
|
func (t *TelegramDaemon) NewAuthToken() string {
|
||||||
|
pin := genAuthToken()
|
||||||
|
t.tokens = append(t.tokens, pin)
|
||||||
|
return pin
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TelegramDaemon) run() {
|
func (t *TelegramDaemon) run() {
|
||||||
t.app.info.Println("Starting Telegram bot daemon")
|
t.app.info.Println("Starting Telegram bot daemon")
|
||||||
u := tg.NewUpdate(0)
|
u := tg.NewUpdate(0)
|
||||||
@ -79,6 +83,7 @@ func (t *TelegramDaemon) run() {
|
|||||||
updates, err := t.bot.GetUpdatesChan(u)
|
updates, err := t.bot.GetUpdatesChan(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
||||||
|
telegramEnabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
@ -171,7 +176,7 @@ func (t *TelegramDaemon) Shutdown() {
|
|||||||
|
|
||||||
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
|
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
|
||||||
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||||
content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
|
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
|
||||||
err := t.Reply(upd, content)
|
err := t.Reply(upd, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||||
@ -180,9 +185,9 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin
|
|||||||
|
|
||||||
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
|
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
|
||||||
if len(sects) == 1 {
|
if len(sects) == 1 {
|
||||||
list := "/lang <lang>\n"
|
list := "/lang `<lang>`\n"
|
||||||
for code := range t.app.storage.lang.Telegram {
|
for code := range t.app.storage.lang.Telegram {
|
||||||
list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
list += fmt.Sprintf("`%s`: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
||||||
}
|
}
|
||||||
err := t.Reply(upd, list)
|
err := t.Reply(upd, list)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -196,8 +201,7 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
|
|||||||
if user.ChatID == upd.Message.Chat.ID {
|
if user.ChatID == upd.Message.Chat.ID {
|
||||||
user.Lang = sects[1]
|
user.Lang = sects[1]
|
||||||
t.app.storage.telegram[jfID] = user
|
t.app.storage.telegram[jfID] = user
|
||||||
err := t.app.storage.storeTelegramUsers()
|
if err := t.app.storage.storeTelegramUsers(); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -225,7 +229,7 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
|
t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
|
||||||
Token: upd.Message.Text,
|
Token: upd.Message.Text,
|
||||||
ChatID: upd.Message.Chat.ID,
|
ChatID: upd.Message.Chat.ID,
|
||||||
Username: upd.Message.Chat.UserName,
|
Username: upd.Message.Chat.UserName,
|
||||||
|
@ -66,6 +66,10 @@ window.availableProfiles = window.availableProfiles || [];
|
|||||||
if (window.telegramEnabled) {
|
if (window.telegramEnabled) {
|
||||||
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
|
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
window.modals.discord = new Modal(document.getElementById("modal-discord"));
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
var inviteCreator = new createInvite();
|
var inviteCreator = new createInvite();
|
||||||
|
77
ts/form.ts
@ -8,12 +8,18 @@ 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;
|
||||||
|
discordInviteLink: boolean;
|
||||||
|
discordServerName: string;
|
||||||
userExpiryEnabled: boolean;
|
userExpiryEnabled: boolean;
|
||||||
userExpiryMonths: number;
|
userExpiryMonths: number;
|
||||||
userExpiryDays: number;
|
userExpiryDays: number;
|
||||||
@ -68,7 +74,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 +90,66 @@ if (window.telegramEnabled) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DiscordInvite {
|
||||||
|
invite: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordVerified = false;
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
|
||||||
|
const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
|
||||||
|
if (window.discordInviteLink) {
|
||||||
|
_get("/invite/" + window.code + "/discord/invite", null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inv = req.response as DiscordInvite;
|
||||||
|
const link = document.getElementById("discord-invite") as HTMLAnchorElement;
|
||||||
|
link.classList.add("subheading", "link-center");
|
||||||
|
link.href = inv.invite;
|
||||||
|
link.target = "_blank";
|
||||||
|
link.innerHTML = `<span class="img-circle lg mr-1"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
discordButton.onclick = () => {
|
||||||
|
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 +227,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 +247,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;
|
||||||
|
@ -2,17 +2,22 @@ import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateStri
|
|||||||
import { templateEmail } from "../modules/settings.js";
|
import { templateEmail } from "../modules/settings.js";
|
||||||
import { Marked } from "@ts-stack/markdown";
|
import { Marked } from "@ts-stack/markdown";
|
||||||
import { stripMarkdown } from "../modules/stripmd.js";
|
import { stripMarkdown } from "../modules/stripmd.js";
|
||||||
|
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
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 {
|
||||||
@ -20,6 +25,8 @@ interface getPinResponse {
|
|||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var addDiscord: (passData: string) => void;
|
||||||
|
|
||||||
class user implements User {
|
class user implements User {
|
||||||
private _row: HTMLTableRowElement;
|
private _row: HTMLTableRowElement;
|
||||||
private _check: HTMLInputElement;
|
private _check: HTMLInputElement;
|
||||||
@ -27,16 +34,21 @@ 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;
|
||||||
private _lastActiveUnix: number;
|
private _lastActiveUnix: number;
|
||||||
id: string;
|
id = "";
|
||||||
private _selected: boolean;
|
private _selected: boolean;
|
||||||
|
|
||||||
get selected(): boolean { return this._selected; }
|
get selected(): boolean { return this._selected; }
|
||||||
@ -82,6 +94,21 @@ 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;
|
||||||
|
if (email) {
|
||||||
|
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,31 +117,39 @@ 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 = `
|
||||||
|
<div class="table-inline">
|
||||||
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
||||||
|
`;
|
||||||
|
if (!window.discordEnabled || !this._discordUsername) {
|
||||||
|
innerHTML += `
|
||||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||||
<div class="dropdown manual">
|
<div class="dropdown manual">
|
||||||
<div class="dropdown-display">
|
<div class="dropdown-display lg">
|
||||||
<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="row 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="row 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
innerHTML += "</div>";
|
||||||
|
this._telegram.innerHTML = innerHTML;
|
||||||
|
if (!window.discordEnabled || !this._discordUsername) {
|
||||||
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
||||||
const button = this._telegram.querySelector("i");
|
const button = this._telegram.querySelector("i");
|
||||||
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
||||||
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 = () => {
|
||||||
@ -129,42 +164,140 @@ class user implements User {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get notify_telegram(): boolean { return this._notifyTelegram; }
|
get notify_telegram(): boolean { return this._notifyTelegram; }
|
||||||
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;
|
if (telegram) {
|
||||||
radios[1].checked = s;
|
telegram.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 = () => addDiscord(this.id);
|
||||||
|
} else {
|
||||||
|
let innerHTML = `
|
||||||
|
<div class="table-inline">
|
||||||
|
<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 lg">
|
||||||
|
<div class="card ~neutral !low">
|
||||||
|
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||||
|
<label class="row switch pb-1 mt-half">
|
||||||
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||||
|
<span>Email</span>
|
||||||
|
</label>
|
||||||
|
<label class="row switch pb-1">
|
||||||
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
||||||
|
<span>Discord</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
if (window.telegramEnabled && this._telegramUsername != "") {
|
||||||
|
innerHTML += `
|
||||||
|
<label class="row switch pb-1">
|
||||||
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
|
||||||
|
<span>Telegram</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
innerHTML += `
|
||||||
|
</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.
|
||||||
|
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) {
|
||||||
|
if (!window.discordEnabled || this._discordUsername == "") return;
|
||||||
|
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;
|
||||||
@ -192,14 +325,19 @@ class user implements User {
|
|||||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||||
let innerHTML = `
|
let innerHTML = `
|
||||||
<td><input type="checkbox" value=""></td>
|
<td><input type="checkbox" value=""></td>
|
||||||
<td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
|
<td><div class="table-inline"><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></td>
|
||||||
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td>
|
<td><div class="table-inline"><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></div></td>
|
||||||
`;
|
`;
|
||||||
if (window.telegramEnabled) {
|
if (window.telegramEnabled) {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<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 +351,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; }
|
||||||
@ -319,12 +458,16 @@ class user implements User {
|
|||||||
this.id = user.id;
|
this.id = user.id;
|
||||||
this.name = user.name;
|
this.name = user.name;
|
||||||
this.email = user.email || "";
|
this.email = user.email || "";
|
||||||
|
this.discord = user.discord;
|
||||||
this.telegram = user.telegram;
|
this.telegram = user.telegram;
|
||||||
this.last_active = user.last_active;
|
this.last_active = user.last_active;
|
||||||
this.admin = user.admin;
|
this.admin = user.admin;
|
||||||
this.disabled = user.disabled;
|
this.disabled = user.disabled;
|
||||||
this.expiry = user.expiry;
|
this.expiry = user.expiry;
|
||||||
|
this.notify_discord = user.notify_discord;
|
||||||
this.notify_telegram = user.notify_telegram;
|
this.notify_telegram = user.notify_telegram;
|
||||||
|
this.notify_email = user.notify_email;
|
||||||
|
this.discord_id = user.discord_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||||
@ -935,6 +1078,19 @@ export class accountsList {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._announceTextarea.onkeyup = this.loadPreview;
|
this._announceTextarea.onkeyup = this.loadPreview;
|
||||||
|
addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => {
|
||||||
|
_post("/users/discord", {jf_id: id, discord_id: user.id}, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
document.dispatchEvent(new CustomEvent("accounts-reload"));
|
||||||
|
if (req.status != 200) {
|
||||||
|
window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs"));
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected"));
|
||||||
|
window.modals.discord.close()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
|
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
|
||||||
|
79
ts/modules/discord.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {addLoader, removeLoader, _get} from "../modules/common.js";
|
||||||
|
|
||||||
|
export interface DiscordUser {
|
||||||
|
name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
var listeners: { [buttonText: string]: (event: CustomEvent) => void } = {};
|
||||||
|
|
||||||
|
export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): (passData: string) => void {
|
||||||
|
if (!window.discordEnabled) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
let timer: NodeJS.Timer;
|
||||||
|
listeners[buttonText] = (event: CustomEvent) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
const list = document.getElementById("discord-list") as HTMLTableElement;
|
||||||
|
const input = document.getElementById("discord-search") as HTMLInputElement;
|
||||||
|
if (input.value.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = ``;
|
||||||
|
addLoader(list);
|
||||||
|
list.parentElement.classList.add("mb-1", "mt-1");
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
_get("/users/discord/" + input.value, null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 200) {
|
||||||
|
removeLoader(list);
|
||||||
|
list.parentElement.classList.remove("mb-1", "mt-1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const users = req.response["users"] as Array<DiscordUser>;
|
||||||
|
let innerHTML = ``;
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
innerHTML += `
|
||||||
|
<tr>
|
||||||
|
<td class="img-circle sm">
|
||||||
|
<img class="img-circle" src="${users[i].avatar_url}" width="32" height="32">
|
||||||
|
</td>
|
||||||
|
<td class="w-100 sm">
|
||||||
|
<p class="content">${users[i].name}</p>
|
||||||
|
</td>
|
||||||
|
<td class="sm">
|
||||||
|
<span id="discord-user-${users[i].id}" class="button ~info !high">${buttonText}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
list.innerHTML = innerHTML;
|
||||||
|
removeLoader(list);
|
||||||
|
list.parentElement.classList.remove("mb-1", "mt-1");
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
const button = document.getElementById(`discord-user-${users[i].id}`) as HTMLInputElement;
|
||||||
|
button.onclick = () => buttonFunction(users[i], event.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 750);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (passData: string) => {
|
||||||
|
const input = document.getElementById("discord-search") as HTMLInputElement;
|
||||||
|
const list = document.getElementById("discord-list") as HTMLDivElement;
|
||||||
|
const header = document.getElementById("discord-header") as HTMLSpanElement;
|
||||||
|
const desc = document.getElementById("discord-description") as HTMLParagraphElement;
|
||||||
|
desc.textContent = description;
|
||||||
|
header.textContent = title;
|
||||||
|
list.innerHTML = ``;
|
||||||
|
input.value = "";
|
||||||
|
for (let key in listeners) {
|
||||||
|
input.removeEventListener("keyup", listeners[key]);
|
||||||
|
}
|
||||||
|
input.addEventListener("keyup", listeners[buttonText].bind(null, { detail: passData }));
|
||||||
|
|
||||||
|
window.modals.discord.show();
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
|
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
|
||||||
|
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
||||||
|
|
||||||
class DOMInvite implements Invite {
|
class DOMInvite implements Invite {
|
||||||
updateNotify = (checkbox: HTMLInputElement) => {
|
updateNotify = (checkbox: HTMLInputElement) => {
|
||||||
@ -25,6 +26,7 @@ class DOMInvite implements Invite {
|
|||||||
document.dispatchEvent(inviteDeletedEvent);
|
document.dispatchEvent(inviteDeletedEvent);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private _label: string = "";
|
private _label: string = "";
|
||||||
get label(): string { return this._label; }
|
get label(): string { return this._label; }
|
||||||
set label(label: string) {
|
set label(label: string) {
|
||||||
@ -82,10 +84,10 @@ class DOMInvite implements Invite {
|
|||||||
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
|
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _email: string = "";
|
private _send_to: string = "";
|
||||||
get email(): string { return this._email };
|
get send_to(): string { return this._send_to };
|
||||||
set email(address: string) {
|
set send_to(address: string) {
|
||||||
this._email = address;
|
this._send_to = address;
|
||||||
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
|
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
|
||||||
const icon = container.querySelector("i");
|
const icon = container.querySelector("i");
|
||||||
const chip = container.querySelector("span.inv-email-chip");
|
const chip = container.querySelector("span.inv-email-chip");
|
||||||
@ -100,7 +102,7 @@ class DOMInvite implements Invite {
|
|||||||
} else {
|
} else {
|
||||||
container.classList.add("mr-1");
|
container.classList.add("mr-1");
|
||||||
chip.classList.add("chip");
|
chip.classList.add("chip");
|
||||||
if (address.includes("Failed to send to")) {
|
if (address.includes("Failed")) {
|
||||||
icon.classList.remove("ri-mail-line");
|
icon.classList.remove("ri-mail-line");
|
||||||
icon.classList.add("ri-mail-close-line");
|
icon.classList.add("ri-mail-close-line");
|
||||||
chip.classList.remove("~neutral");
|
chip.classList.remove("~neutral");
|
||||||
@ -372,7 +374,7 @@ class DOMInvite implements Invite {
|
|||||||
update = (invite: Invite) => {
|
update = (invite: Invite) => {
|
||||||
this.code = invite.code;
|
this.code = invite.code;
|
||||||
this.created = invite.created;
|
this.created = invite.created;
|
||||||
this.email = invite.email;
|
this.send_to = invite.send_to;
|
||||||
this.expiresIn = invite.expiresIn;
|
this.expiresIn = invite.expiresIn;
|
||||||
if (window.notificationsEnabled) {
|
if (window.notificationsEnabled) {
|
||||||
this.notifyCreation = invite.notifyCreation;
|
this.notifyCreation = invite.notifyCreation;
|
||||||
@ -482,7 +484,7 @@ export class inviteList implements inviteList {
|
|||||||
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
|
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
|
||||||
let parsed: Invite = {};
|
let parsed: Invite = {};
|
||||||
parsed.code = invite["code"] as string;
|
parsed.code = invite["code"] as string;
|
||||||
parsed.email = invite["email"] as string || "";
|
parsed.send_to = invite["send_to"] as string || "";
|
||||||
parsed.label = invite["label"] as string || "";
|
parsed.label = invite["label"] as string || "";
|
||||||
let time = "";
|
let time = "";
|
||||||
let userExpiryTime = "";
|
let userExpiryTime = "";
|
||||||
@ -520,6 +522,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
|
|||||||
export class createInvite {
|
export class createInvite {
|
||||||
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
|
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
|
||||||
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
|
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
|
||||||
|
private _discordSearch: HTMLSpanElement;
|
||||||
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
|
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
|
||||||
private _uses = document.getElementById('create-uses') as HTMLInputElement;
|
private _uses = document.getElementById('create-uses') as HTMLInputElement;
|
||||||
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
|
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
|
||||||
@ -542,6 +545,8 @@ export class createInvite {
|
|||||||
private _invDuration = document.getElementById('inv-duration');
|
private _invDuration = document.getElementById('inv-duration');
|
||||||
private _userExpiry = document.getElementById('user-expiry');
|
private _userExpiry = document.getElementById('user-expiry');
|
||||||
|
|
||||||
|
private _sendToDiscord: (passData: string) => void;
|
||||||
|
|
||||||
// Broadcast when new invite created
|
// Broadcast when new invite created
|
||||||
private _newInviteEvent = new CustomEvent("newInviteEvent");
|
private _newInviteEvent = new CustomEvent("newInviteEvent");
|
||||||
private _firstLoad = true;
|
private _firstLoad = true;
|
||||||
@ -576,9 +581,19 @@ export class createInvite {
|
|||||||
if (state) {
|
if (state) {
|
||||||
this._sendToEnabled.parentElement.classList.remove("~neutral");
|
this._sendToEnabled.parentElement.classList.remove("~neutral");
|
||||||
this._sendToEnabled.parentElement.classList.add("~urge");
|
this._sendToEnabled.parentElement.classList.add("~urge");
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
this._discordSearch.classList.remove("~neutral");
|
||||||
|
this._discordSearch.classList.add("~urge");
|
||||||
|
this._discordSearch.onclick = () => this._sendToDiscord("");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._sendToEnabled.parentElement.classList.remove("~urge");
|
this._sendToEnabled.parentElement.classList.remove("~urge");
|
||||||
this._sendToEnabled.parentElement.classList.add("~neutral");
|
this._sendToEnabled.parentElement.classList.add("~neutral");
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
this._discordSearch.classList.remove("~urge");
|
||||||
|
this._discordSearch.classList.add("~neutral");
|
||||||
|
this._discordSearch.onclick = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -732,7 +747,7 @@ export class createInvite {
|
|||||||
"multiple-uses": (this.uses > 1 || this.infiniteUses),
|
"multiple-uses": (this.uses > 1 || this.infiniteUses),
|
||||||
"no-limit": this.infiniteUses,
|
"no-limit": this.infiniteUses,
|
||||||
"remaining-uses": this.uses,
|
"remaining-uses": this.uses,
|
||||||
"email": this.sendToEnabled ? this.sendTo : "",
|
"send-to": this.sendToEnabled ? this.sendTo : "",
|
||||||
"profile": this.profile,
|
"profile": this.profile,
|
||||||
"label": this.label
|
"label": this.label
|
||||||
};
|
};
|
||||||
@ -761,7 +776,6 @@ export class createInvite {
|
|||||||
this._userDays.disabled = true;
|
this._userDays.disabled = true;
|
||||||
this._userHours.disabled = true;
|
this._userHours.disabled = true;
|
||||||
this._userMinutes.disabled = true;
|
this._userMinutes.disabled = true;
|
||||||
this.sendToEnabled = false;
|
|
||||||
this._createButton.onclick = this.create;
|
this._createButton.onclick = this.create;
|
||||||
this.sendTo = "";
|
this.sendTo = "";
|
||||||
this.uses = 1;
|
this.uses = 1;
|
||||||
@ -798,11 +812,22 @@ export class createInvite {
|
|||||||
this._minutes.onchange = this._checkDurationValidity;
|
this._minutes.onchange = this._checkDurationValidity;
|
||||||
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
||||||
|
|
||||||
if (!window.emailEnabled) {
|
if (!window.emailEnabled && !window.discordEnabled) {
|
||||||
document.getElementById("create-send-to-container").classList.add("unfocused");
|
document.getElementById("create-send-to-container").classList.add("unfocused");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
this._discordSearch = document.getElementById("create-send-to-search") as HTMLSpanElement;
|
||||||
|
this._sendToDiscord = newDiscordSearch(
|
||||||
|
window.lang.strings("findDiscordUser"),
|
||||||
|
window.lang.strings("searchDiscordUser"),
|
||||||
|
window.lang.strings("select"),
|
||||||
|
(user: DiscordUser) => {
|
||||||
|
this.sendTo = user.name;
|
||||||
|
window.modals.discord.close();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.sendToEnabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
@ -101,13 +102,14 @@ declare interface Modals {
|
|||||||
extendExpiry: Modal;
|
extendExpiry: Modal;
|
||||||
updateInfo: Modal;
|
updateInfo: Modal;
|
||||||
telegram: Modal;
|
telegram: Modal;
|
||||||
|
discord: Modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Invite {
|
interface Invite {
|
||||||
code?: string;
|
code?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
remainingUses?: string;
|
remainingUses?: string;
|
||||||
email?: string;
|
send_to?: string;
|
||||||
usedBy?: { [name: string]: number };
|
usedBy?: { [name: string]: number };
|
||||||
created?: number;
|
created?: number;
|
||||||
notifyExpiry?: boolean;
|
notifyExpiry?: boolean;
|
||||||
|
25
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,
|
||||||
@ -256,8 +258,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
email := app.storage.invites[code].Email
|
email := app.storage.invites[code].SendTo
|
||||||
if strings.Contains(email, "Failed") {
|
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
@ -284,13 +286,30 @@ 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,
|
||||||
|
}))
|
||||||
|
data["discordServerName"] = app.discord.serverName
|
||||||
|
data["discordInviteLink"] = app.discord.inviteChannelName != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// if discordEnabled {
|
||||||
|
// pin := ""
|
||||||
|
// for _, token := range app.discord.tokens {
|
||||||
|
// if
|
||||||
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|