1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 17:10:11 +00:00

Compare commits

..

No commits in common. "8b62c91d130ad088d9ad3a4f4257262a6d405b19" and "478b40d0ff7122d79df54b25a3118cfd4640f6e5" have entirely different histories.

40 changed files with 408 additions and 1810 deletions

View File

@ -52,13 +52,6 @@ else
TYPECHECK := TYPECHECK :=
endif endif
RACE ?= off
ifeq ($(RACE), on)
RACEDETECTOR := -race
else
RACEDETECTOR :=
endif
npm: npm:
$(info installing npm dependencies) $(info installing npm dependencies)
npm install npm install
@ -98,7 +91,7 @@ compile:
$(GOBINARY) mod download $(GOBINARY) mod download
$(info Building) $(info Building)
mkdir -p build mkdir -p build
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go $(GOBINARY) build -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
compress: compress:
upx --lzma build/jfa-go upx --lzma build/jfa-go

View File

@ -17,7 +17,7 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too. * ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions. * 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason. * Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
* Telegram & Discord Integration: Verify users via a Telegram or Discord bot, and send Password Resets, Announcements, etc. through it. * Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation. * 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
* Email addresses can optionally be used instead of usernames * Email addresses can optionally be used instead of usernames
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram. * 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram.
@ -54,8 +54,6 @@ docker create \
-v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct -v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct
hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git
``` ```
`TrayIcon` builds include a tray icon to start/stop/restart, and an option to automatically start when you log-in to your computer. For Linux users, these builds depend on the `libappindicator3-1`/`libappindicator-gtk3`/`libappindicator` package for Debian/Ubuntu, Fedora, and Alpine respectively.
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/), [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/). Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/), [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.dev/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful. For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.dev/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful.

272
api.go
View File

@ -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.Addr email = e.(string)
} }
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] = EmailAddress{Addr: req.Email, Contact: true} app.storage.emails[id] = req.Email
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,30 +330,6 @@ 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 == "" {
@ -478,7 +454,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] = EmailAddress{Addr: req.Email, Contact: true} app.storage.emails[id] = req.Email
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -503,18 +479,7 @@ 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{
@ -529,7 +494,8 @@ 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
if err := app.storage.storeTelegramUsers(); err != nil { err := app.storage.storeTelegramUsers()
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]
@ -537,7 +503,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 || discordVerified { if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 {
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)
@ -826,44 +792,18 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.UserMinutes = req.UserMinutes invite.UserMinutes = req.UserMinutes
} }
invite.ValidTill = validTill invite.ValidTill = validTill
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
addressValid := false app.debug.Printf("%s: Sending invite email", inviteCode)
discord := "" invite.Email = req.Email
app.debug.Printf("%s: Sending invite message", inviteCode) msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if discordEnabled && !strings.Contains(req.SendTo, "@") { if err != nil {
users := app.discord.GetUsers(req.SendTo) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
if len(users) == 0 { app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err)
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo) } else if err := app.email.send(msg, req.Email); err != nil {
} else if len(users) > 1 { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo) app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
} else { } else {
invite.SendTo = req.SendTo app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email)
addressValid = true
discord = users[0].User.ID
}
} else if emailEnabled {
addressValid = true
invite.SendTo = req.SendTo
}
if addressValid {
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
} else {
var err error
if discord != "" {
err = app.discord.SendDM(msg, discord)
} else {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
} else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
}
}
} }
} }
if req.Profile != "" { if req.Profile != "" {
@ -927,15 +867,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.SendTo != "" { if inv.Email != "" {
invite.SendTo = inv.SendTo invite.Email = inv.Email
} }
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, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" { if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
address = addr.Addr address = addr.(string)
} }
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
@ -1134,14 +1074,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) {
addr, ok := app.storage.emails[gc.GetString("jfId")] var ok bool
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()
} }
@ -1228,8 +1168,7 @@ 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.Addr user.Email = email.(string)
user.NotifyThroughEmail = email.Contact
} }
expiry, ok := app.storage.users[jfUser.ID] expiry, ok := app.storage.users[jfUser.ID]
if ok { if ok {
@ -1239,11 +1178,6 @@ 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++
} }
@ -1319,11 +1253,7 @@ 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 {
contact := true app.storage.emails[id] = address
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 {
@ -2074,63 +2004,38 @@ 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 SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram." // @Param telegramNotifyDTO body telegramNotifyDTO 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) SetContactMethods(gc *gin.Context) { func (app *appContext) TelegramSetNotify(gc *gin.Context) {
var req SetContactMethodsDTO var req telegramNotifyDTO
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.Telegram tgUser.Contact = req.Enabled
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.Telegram { if !req.Enabled {
msg = " not" msg = "not"
} }
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg) app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
return
} }
if dcUser, ok := app.storage.discord[req.ID]; ok { app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
dcUser.Contact = req.Discord respondBool(400, false, gc)
app.storage.discord[req.ID] = dcUser
if err := app.storage.storeDiscordUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Discord: Failed to store users: %v", err)
return
}
msg := ""
if !req.Discord {
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
}
if email, ok := app.storage.emails[req.ID]; ok {
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
respondBool(200, true, gc)
} }
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth. // @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
@ -2187,107 +2092,6 @@ 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

View File

@ -14,7 +14,6 @@ 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("")
@ -43,7 +42,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", "discord_users"} { for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_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(""), "/")
@ -88,17 +87,15 @@ 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 && !discordEnabled { if !emailEnabled && !telegramEnabled {
messagesEnabled = false messagesEnabled = false
} }
@ -171,28 +168,3 @@ 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
}

View File

@ -546,89 +546,6 @@
} }
} }
}, },
"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": {
@ -648,7 +565,6 @@
"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."
@ -1224,14 +1140,6 @@
"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."
} }
} }
} }

View File

@ -30,12 +30,12 @@
} }
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 750px) {
:root { :root {
font-size: 0.9rem; font-size: 0.9rem;
} }
.table-responsive table { .table-responsive table {
min-width: 800px; min-width: 660px;
} }
} }
@ -130,10 +130,6 @@ 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;
} }
@ -176,7 +172,7 @@ div.card:contains(section.banner.footer) {
} }
p.sm, p.sm,
span.sm:not(.heading) { span.sm {
font-size: 0.75rem; font-size: 0.75rem;
} }
@ -428,7 +424,6 @@ p.top {
.table-responsive { .table-responsive {
overflow-x: auto; overflow-x: auto;
font-size: 0.9rem;
} }
#notification-box { #notification-box {
@ -443,10 +438,6 @@ 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 */
@ -492,32 +483,3 @@ 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;
}

View File

@ -1,410 +0,0 @@
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
}
// Wait for everything to populate, it's slow sometimes.
for d.bot.State == nil {
continue
}
for d.bot.State.User == nil {
continue
}
d.username = d.bot.State.User.Username
for d.bot.State.Guilds == nil {
continue
}
// 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
}

View File

@ -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 || discordEnabled { if telegramEnabled {
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 || discordEnabled { if telegramEnabled {
keys = []string{"html", "text", "markdown"} keys = []string{"html", "text", "markdown"}
} else { } else {
keys = []string{"html", "text"} keys = []string{"html", "text"}
@ -807,21 +807,8 @@ 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)
if err != nil { } else if address, ok := app.storage.emails[id]; ok {
return err err = app.email.send(email, address.(string))
}
}
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
@ -831,14 +818,11 @@ 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.Addr return addr.(string)
} }
return "" return ""
} }

1
go.mod
View File

@ -11,7 +11,6 @@ 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
View File

@ -11,8 +11,6 @@ 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=
@ -150,8 +148,6 @@ 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=
@ -269,7 +265,6 @@ 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=

View File

@ -7,7 +7,6 @@
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 }});
@ -328,18 +327,6 @@
</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">&times;</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">
@ -501,14 +488,7 @@
<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>
@ -545,9 +525,6 @@
{{ 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>

View File

@ -17,11 +17,6 @@
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 }}

View File

@ -37,17 +37,6 @@
</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>
@ -80,25 +69,13 @@
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -20,8 +20,6 @@
"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",
@ -96,9 +94,7 @@
"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}.",
@ -111,7 +107,6 @@
"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.",

View File

@ -17,8 +17,6 @@
"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"
} }
} }

View File

@ -18,16 +18,14 @@
"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.",
"errorDiscordVerification": "Discord verification required.", "errorInvalidPIN": "Telegram PIN is invalid.",
"errorInvalidPIN": "PIN is invalid.", "telegramVerified": "Telegram account verified."
"verified": "Account verified."
}, },
"validationStrings": { "validationStrings": {
"length": { "length": {

View File

@ -15,7 +15,7 @@
"passwordRequirementsHeader": "Requisitos da Senha", "passwordRequirementsHeader": "Requisitos da Senha",
"successHeader": "Sucesso!", "successHeader": "Sucesso!",
"successContinueButton": "Continuar", "successContinueButton": "Continuar",
"confirmationRequired": "Confirmação por e-mail", "confirmationRequired": "Necessária confirmação de e-mail",
"confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro.", "confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro.",
"yourAccountIsValidUntil": "Sua conta é válida até {date}.", "yourAccountIsValidUntil": "Sua conta é válida até {date}.",
"sendPIN": "Envie o PIN abaixo para o bot e volte aqui para vincular sua conta." "sendPIN": "Envie o PIN abaixo para o bot e volte aqui para vincular sua conta."

View File

@ -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 {command}, and set language with {command} <language code>." "languageMessage": "Note: See available languages with /lang, and set language with /lang <language code>."
} }
} }

171
main.go
View File

@ -16,6 +16,7 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
@ -95,7 +96,6 @@ 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,10 +320,6 @@ 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 {
@ -345,10 +341,6 @@ 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()
@ -437,76 +429,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
@ -570,22 +562,11 @@ 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"
@ -621,12 +602,12 @@ func start(asDaemon, firstCall bool) {
app.err.Printf("Failure serving: %s", err) app.err.Printf("Failure serving: %s", err)
} }
} }
if firstRun {
app.info.Printf("Loaded, visit %s to start.", address)
} else {
app.info.Printf("Loaded @ %s", address)
}
}() }()
if firstRun {
app.info.Printf("Loaded, visit %s to start.", address)
} else {
app.info.Printf("Loaded @ %s", address)
}
app.quit = make(chan os.Signal) app.quit = make(chan os.Signal)
signal.Notify(app.quit, os.Interrupt) signal.Notify(app.quit, os.Interrupt)
go func() { go func() {
@ -779,7 +760,7 @@ You can then run:
fmt.Print(info("systemctl --user stop jfa-go\n\n")) fmt.Print(info("systemctl --user stop jfa-go\n\n"))
color.New(color.FgYellow).PrintFunc()("To restart: ") color.New(color.FgYellow).PrintFunc()("To restart: ")
fmt.Print(info("systemctl --user stop jfa-go\n")) fmt.Print(info("systemctl --user stop jfa-go\n"))
} else if TRAY { } else if TRAY && flagPassed("tray") {
RunTray() RunTray()
} else { } else {
RESTART = make(chan bool, 1) RESTART = make(chan bool, 1)

View File

@ -17,8 +17,6 @@ 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 {
@ -50,7 +48,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
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
@ -100,7 +98,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)
SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable) Email string `json:"email,omitempty"` // Email 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
@ -127,16 +125,12 @@ 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 {
@ -255,29 +249,7 @@ type telegramSetDTO struct {
ID string `json:"id"` // Jellyfin ID of user. ID string `json:"id"` // Jellyfin ID of user.
} }
type SetContactMethodsDTO struct { type telegramNotifyDTO struct {
ID string `json:"id"` ID string `json:"id"`
Email bool `json:"email"` Enabled bool `json:"enabled"`
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"`
} }

View File

@ -121,12 +121,6 @@ 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"))
@ -164,15 +158,11 @@ 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 || discordEnabled { if telegramEnabled {
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/contact", app.SetContactMethods) api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
}
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)

View File

@ -15,21 +15,19 @@ 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, discord_path string invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_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
displayprefs, ombi_template map[string]interface{} emails, 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. customEmails customEmails
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users. policy mediabrowser.Policy
customEmails customEmails configuration mediabrowser.Configuration
policy mediabrowser.Policy lang Lang
configuration mediabrowser.Configuration invitesLock, usersLock sync.Mutex
lang Lang
invitesLock, usersLock sync.Mutex
} }
type TelegramUser struct { type TelegramUser struct {
@ -39,20 +37,6 @@ 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"`
@ -95,7 +79,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"`
SendTo string `json:"email"` Email string `json:"email"`
// Used to be stored as formatted time, now as Unix. // Used to be stored as formatted time, now as Unix.
UsedBy [][]string `json:"used-by"` UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"` Notify map[string]map[string]bool `json:"notify"`
@ -105,24 +89,23 @@ type Invite struct {
} }
type Lang struct { type Lang struct {
AdminPath string AdminPath string
chosenAdminLang string chosenAdminLang string
Admin adminLangs Admin adminLangs
AdminJSON map[string]string AdminJSON map[string]string
FormPath string FormPath string
chosenFormLang string chosenFormLang string
Form formLangs Form formLangs
PasswordResetPath string PasswordResetPath string
chosenPWRLang string chosenPWRLang string
PasswordReset pwrLangs PasswordReset pwrLangs
EmailPath string EmailPath string
chosenEmailLang string chosenEmailLang string
Email emailLangs Email emailLangs
CommonPath string CommonPath string
Common commonLangs Common commonLangs
SetupPath string SetupPath string
Setup setupLangs Setup setupLangs
// Telegram translations are also used for Discord bots (and likely future ones).
chosenTelegramLang string chosenTelegramLang string
TelegramPath string TelegramPath string
Telegram telegramLangs Telegram telegramLangs
@ -782,14 +765,6 @@ 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)
} }
@ -909,85 +884,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
// } }

View File

@ -3,57 +3,22 @@ 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.
// If links = true, links are completely removed, and a list of URLs and their alt text is also returned. func StripAltText(md string) string {
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] out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+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
} }
@ -71,12 +36,11 @@ func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
out += md[previousURLEnd+2:] out += md[previousURLEnd+2:]
} }
if out == "" { if out == "" {
return md, embeds return md
} }
return out, embeds return out
} }
func stripMarkdown(md string) string { func stripMarkdown(md string) string {
stripped, _ := StripAltText(md, false) return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "</p>"), "<p>")
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "</p>"), "<p>")
} }

View File

@ -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 TelegramVerifiedToken struct { type VerifiedToken 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 []TelegramVerifiedToken verifiedTokens []VerifiedToken
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,11 +37,12 @@ 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: []TelegramVerifiedToken{}, verifiedTokens: []VerifiedToken{},
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,
@ -54,7 +55,10 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return td, nil return td, nil
} }
func genAuthToken() string { var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// 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 {
@ -64,18 +68,10 @@ func genAuthToken() 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)
@ -83,7 +79,6 @@ 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 {
@ -176,7 +171,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.template("languageMessage", tmpl{"command": "/lang"}) content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
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)
@ -185,9 +180,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 {
@ -201,7 +196,8 @@ 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
if err := t.app.storage.storeTelegramUsers(); err != nil { err := t.app.storage.storeTelegramUsers()
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
@ -229,7 +225,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, TelegramVerifiedToken{ t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
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,

View File

@ -66,10 +66,6 @@ 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();

View File

@ -8,18 +8,12 @@ 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;
@ -74,7 +68,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["verified"]); window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
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");
@ -90,66 +84,6 @@ 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);
} }
@ -227,8 +161,6 @@ 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) => {
@ -247,13 +179,6 @@ 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;

View File

@ -2,30 +2,23 @@ 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 {
token: string; token: string;
username: string; username: string;
} }
var addDiscord: (passData: string) => void;
class user implements User { class user implements User {
private _row: HTMLTableRowElement; private _row: HTMLTableRowElement;
@ -34,21 +27,16 @@ class user implements User {
private _admin: HTMLSpanElement; private _admin: HTMLSpanElement;
private _disabled: HTMLSpanElement; private _disabled: HTMLSpanElement;
private _email: HTMLInputElement; private _email: HTMLInputElement;
private _notifyEmail: boolean;
private _emailAddress: string; private _emailAddress: string;
private _emailEditButton: HTMLElement; private _emailEditButton: HTMLElement;
private _telegram: HTMLTableDataCellElement; private _telegram: HTMLTableDataCellElement;
private _telegramUsername: string; private _telegramUsername: string;
private _notifyTelegram: boolean; private _notifyTelegram: boolean;
private _discord: HTMLTableDataCellElement;
private _discordUsername: string;
private _discordID: string;
private _notifyDiscord: boolean;
private _expiry: HTMLTableDataCellElement; private _expiry: HTMLTableDataCellElement;
private _expiryUnix: number; private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement; private _lastActive: HTMLTableDataCellElement;
private _lastActiveUnix: number; private _lastActiveUnix: number;
id = ""; id: string;
private _selected: boolean; private _selected: boolean;
get selected(): boolean { return this._selected; } get selected(): boolean { return this._selected; }
@ -93,21 +81,6 @@ class user implements User {
this._email.textContent = value; this._email.textContent = value;
} }
} }
get notify_email(): boolean { return this._notifyEmail; }
set notify_email(s: boolean) {
this._notifyEmail = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
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) {
@ -117,152 +90,31 @@ 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 {
let innerHTML = ` this._telegram.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> <i class="icon ri-settings-2-line ml-half dropdown-button"></i>
`; <div class="dropdown manual">
if (!window.discordEnabled || !this._discordUsername) { <div class="dropdown-display">
innerHTML += ` <div class="card ~neutral !low">
<i class="icon ri-settings-2-line ml-half dropdown-button"></i> <span class="supra sm">${window.lang.strings("contactThrough")}</span>
<div class="dropdown manual"> <label class="switch pb-1 mt-half">
<div class="dropdown-display lg"> <input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
<div class="card ~neutral !low"> <span>Email</span>
<span class="supra sm">${window.lang.strings("contactThrough")}</span> </label>
<label class="row switch pb-1 mt-half"> <label class="switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email"> <input type="radio" name="accounts-contact-${this.id}">
<span>Email</span> <span>Telegram</span>
</label> </label>
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
</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.
const button = this._telegram.querySelector("i");
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod("telegram");
}
button.onclick = () => {
dropdown.classList.add("selected");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
}
}
}
get notify_telegram(): boolean { return this._notifyTelegram; }
set notify_telegram(s: boolean) {
if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s;
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
if (telegram) {
telegram.checked = s;
}
if (window.discordEnabled && this._discordUsername) {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s;
}
}
private _setNotifyMethod = (mode: string = "telegram") => {
let el: HTMLElement;
if (mode == "telegram") { el = this._telegram }
else if (mode == "discord") { el = this._discord }
const email = el.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
let send = {
id: this.id,
email: email.checked
}
if (window.telegramEnabled && this._telegramUsername) {
const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
send["telegram"] = telegram.checked;
}
if (window.discordEnabled && this._discordUsername) {
const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
send["discord"] = discord.checked;
}
_post("/users/contact", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings"));
document.dispatchEvent(new CustomEvent("accounts-reload"));
return;
}
}
}, false, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.connectionError();
document.dispatchEvent(new CustomEvent("accounts-reload"));
return;
} else if (req.status == 401) {
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> </div>
</div> </div>
`; `;
this._discord.innerHTML = innerHTML;
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username. // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
const button = this._discord.querySelector("i"); const button = this._telegram.querySelector("i");
const dropdown = this._discord.querySelector("div.dropdown") as HTMLDivElement; const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
const checks = this._discord.querySelectorAll("input") as NodeListOf<HTMLInputElement>; const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) { for (let i = 0; i < radios.length; i++) {
checks[i].onclick = () => this._setNotifyMethod("discord"); radios[i].onclick = this._setTelegramNotify;
} }
button.onclick = () => { button.onclick = () => {
@ -270,32 +122,47 @@ class user implements User {
document.addEventListener("click", outerClickListener); document.addEventListener("click", outerClickListener);
}; };
const outerClickListener = (event: Event) => { const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._discord.contains(event.target) || button.contains(event.target)))) { if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected"); dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener); 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; } get notify_telegram(): boolean { return this._notifyTelegram; }
set notify_discord(s: boolean) { set notify_telegram(s: boolean) {
if (!window.discordEnabled || !this._discordUsername) return; if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyDiscord = s; this._notifyTelegram = s;
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement; const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
discord.checked = s; radios[0].checked = !s;
if (window.telegramEnabled && this._telegramUsername != "") { radios[1].checked = s;
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement; }
discord.checked = s;
} private _setTelegramNotify = () => {
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
let send = {
id: this.id,
enabled: radios[1].checked
};
_post("/users/telegram/notify", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings"));
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
return;
}
}
}, false, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.connectionError();
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
return;
} else if (req.status == 401) {
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
}
});
} }
get expiry(): number { return this._expiryUnix; } get expiry(): number { return this._expiryUnix; }
@ -325,19 +192,14 @@ 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><div class="table-inline"><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></td> <td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></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> <td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></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>
@ -351,7 +213,6 @@ 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; }
@ -409,7 +270,7 @@ class user implements User {
} }
}); });
} }
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => { private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {
const pin = document.getElementById("telegram-pin"); const pin = document.getElementById("telegram-pin");
@ -458,16 +319,12 @@ 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; }
@ -1078,19 +935,6 @@ 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) => {

View File

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

View File

@ -1,5 +1,4 @@
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) => {
@ -26,7 +25,6 @@ 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) {
@ -84,10 +82,10 @@ class DOMInvite implements Invite {
this._middle.querySelector("strong.inv-remaining").textContent = remaining; this._middle.querySelector("strong.inv-remaining").textContent = remaining;
} }
private _send_to: string = ""; private _email: string = "";
get send_to(): string { return this._send_to }; get email(): string { return this._email };
set send_to(address: string) { set email(address: string) {
this._send_to = address; this._email = 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");
@ -102,7 +100,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")) { if (address.includes("Failed to send to")) {
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");
@ -374,7 +372,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.send_to = invite.send_to; this.email = invite.email;
this.expiresIn = invite.expiresIn; this.expiresIn = invite.expiresIn;
if (window.notificationsEnabled) { if (window.notificationsEnabled) {
this.notifyCreation = invite.notifyCreation; this.notifyCreation = invite.notifyCreation;
@ -484,7 +482,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.send_to = invite["send_to"] as string || ""; parsed.email = invite["email"] as string || "";
parsed.label = invite["label"] as string || ""; parsed.label = invite["label"] as string || "";
let time = ""; let time = "";
let userExpiryTime = ""; let userExpiryTime = "";
@ -522,7 +520,6 @@ 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;
@ -545,8 +542,6 @@ 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;
@ -581,19 +576,9 @@ 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;
}
} }
} }
@ -747,7 +732,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,
"send-to": this.sendToEnabled ? this.sendTo : "", "email": this.sendToEnabled ? this.sendTo : "",
"profile": this.profile, "profile": this.profile,
"label": this.label "label": this.label
}; };
@ -776,6 +761,7 @@ 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;
@ -812,22 +798,11 @@ 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 && !window.discordEnabled) { if (!window.emailEnabled) {
document.getElementById("create-send-to-container").classList.add("unfocused"); document.getElementById("create-send-to-container").classList.add("unfocused");
} }
if (window.discordEnabled) {
this._discordSearch = document.getElementById("create-send-to-search") as HTMLSpanElement;
this._sendToDiscord = newDiscordSearch(
window.lang.strings("findDiscordUser"),
window.lang.strings("searchDiscordUser"),
window.lang.strings("select"),
(user: DiscordUser) => {
this.sendTo = user.name;
window.modals.discord.close();
}
);
}
this.sendToEnabled = false;
} }
} }

View File

@ -21,7 +21,6 @@ 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;
@ -102,14 +101,13 @@ 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;
send_to?: string; email?: string;
usedBy?: { [name: string]: number }; usedBy?: { [name: string]: number };
created?: number; created?: number;
notifyExpiry?: boolean; notifyExpiry?: boolean;

View File

@ -1,8 +1,8 @@
package main package main
import ( import (
"archive/zip" "archive/tar"
"bytes" "compress/gzip"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -184,7 +184,6 @@ func (ud *Updater) GetTag() (Tag, int, error) {
return Tag{}, -1, nil return Tag{}, -1, nil
} }
url := fmt.Sprintf("%s/repo/%s/%s/tag/latest/%s", ud.url, ud.namespace, ud.name, ud.tag) url := fmt.Sprintf("%s/repo/%s/%s/tag/latest/%s", ud.url, ud.namespace, ud.name, ud.tag)
fmt.Println(url)
req, _ := http.NewRequest("GET", url, nil) req, _ := http.NewRequest("GET", url, nil)
resp, err := ud.httpClient.Do(req) resp, err := ud.httpClient.Do(req)
defer ud.timeoutHandler() defer ud.timeoutHandler()
@ -348,11 +347,7 @@ func getBuildName() string {
if arch == "" { if arch == "" {
return "" return ""
} }
tray := "" return operatingSystem + "_" + arch
if TRAY {
tray = "TrayIcon_"
}
return tray + operatingSystem + "_" + arch
} }
func (ud *Updater) downloadInternal(assets *[]GHAsset, tag Tag) (applyUpdate ApplyUpdate, status int, err error) { func (ud *Updater) downloadInternal(assets *[]GHAsset, tag Tag) (applyUpdate ApplyUpdate, status int, err error) {
@ -397,129 +392,69 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) gz, err := gzip.NewReader(resp.Body)
if err != nil { if err != nil {
status = -1 status = -1
return return
} }
zp, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) defer gz.Close()
if err != nil { tarReader := tar.NewReader(gz)
status = -1 var header *tar.Header
return for {
} header, err = tarReader.Next()
for _, zf := range zp.File { if err == io.EOF {
if zf.Name != ud.binary { break
continue
} }
var file string
file, err = os.Executable()
if err != nil { if err != nil {
status = -1
return return
} }
var path string switch header.Typeflag {
path, err = filepath.EvalSymlinks(file) case tar.TypeReg:
if err != nil { // Search only for file named ud.binary
return if header.Name == ud.binary {
} var file string
var info fs.FileInfo file, err = os.Executable()
info, err = os.Stat(path) if err != nil {
if err != nil { return
return }
} var path string
mode := info.Mode() path, err = filepath.EvalSymlinks(file)
var unzippedFile io.ReadCloser if err != nil {
unzippedFile, err = zf.Open() return
if err != nil { }
return var info fs.FileInfo
} info, err = os.Stat(path)
defer unzippedFile.Close() if err != nil {
var f *os.File return
f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) }
if err != nil { mode := info.Mode()
return var f *os.File
} f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
defer f.Close() if err != nil {
_, err = io.Copy(f, unzippedFile) return
if err != nil { }
return defer f.Close()
} _, err = io.Copy(f, tarReader)
applyUpdate = func() error { if err != nil {
oldName := path + "-" + version + "-" + commit return
err := os.Rename(path, oldName) }
if err != nil { applyUpdate = func() error {
return err oldName := path + "-" + version + "-" + commit
err := os.Rename(path, oldName)
if err != nil {
return err
}
err = os.Rename(path+"_", path)
if err != nil {
return err
}
return os.Remove(oldName)
}
return
} }
err = os.Rename(path+"_", path)
if err != nil {
return err
}
return os.Remove(oldName)
} }
return
} }
// gz, err := gzip.NewReader(resp.Body)
// if err != nil {
// status = -1
// return
// }
// defer gz.Close()
// tarReader := tar.NewReader(gz)
// var header *tar.Header
// for {
// header, err = tarReader.Next()
// if err == io.EOF {
// break
// }
// if err != nil {
// status = -1
// return
// }
// switch header.Typeflag {
// case tar.TypeReg:
// // Search only for file named ud.binary
// if header.Name == ud.binary {
// var file string
// file, err = os.Executable()
// if err != nil {
// return
// }
// var path string
// path, err = filepath.EvalSymlinks(file)
// if err != nil {
// return
// }
// var info fs.FileInfo
// info, err = os.Stat(path)
// if err != nil {
// return
// }
// mode := info.Mode()
// var f *os.File
// f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
// if err != nil {
// return
// }
// defer f.Close()
// _, err = io.Copy(f, tarReader)
// if err != nil {
// return
// }
// applyUpdate = func() error {
// oldName := path + "-" + version + "-" + commit
// err := os.Rename(path, oldName)
// if err != nil {
// return err
// }
// err = os.Rename(path+"_", path)
// if err != nil {
// return err
// }
// return os.Remove(oldName)
// }
// return
// }
// }
// }
err = errors.New("Couldn't find file: " + ud.binary) err = errors.New("Couldn't find file: " + ud.binary)
return return
} }

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"html/template"
"io/fs" "io/fs"
"net/http" "net/http"
"strconv" "strconv"
@ -122,7 +121,6 @@ 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,
@ -258,8 +256,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
} }
return return
} }
email := app.storage.invites[code].SendTo email := app.storage.invites[code].Email
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") { if strings.Contains(email, "Failed") {
email = "" email = ""
} }
data := gin.H{ data := gin.H{
@ -286,30 +284,13 @@ 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 telegramEnabled { if data["telegramEnabled"].(bool) {
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)
} }