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

Compare commits

..

34 Commits

Author SHA1 Message Date
8b62c91d13
Mention TrayIcon deps in README 2021-05-23 23:49:48 +01:00
e7d1693517
Enable updater for Tray builds 2021-05-23 23:20:14 +01:00
e78b4882b3
Fix updater for zip files
Forgot to change this when I switched, oops.
2021-05-23 23:05:40 +01:00
e01144950b
Mention Discord in README 2021-05-23 22:41:39 +01:00
86ef665b12
Discord: Try to avoid more race conditions
also added RACE=on to Makefile to enable go's race detector.
2021-05-23 22:26:56 +01:00
f419a57e6d
Fixed loaded message, Tray by default 2021-05-23 22:12:47 +01:00
d7e8ec95de
add missing perms, fix order 2021-05-23 20:48:17 +01:00
Harvey Tindall
5a9bc1c66f
Merge Discord branch
Discord Integration, Accounts UI improvements
2021-05-23 20:25:15 +01:00
1f9af8df89
Discord: Add option to provide server invite
When enabled, a temporary one-use invite is created and shown to the
user on the account creation form.
2021-05-23 19:50:03 +01:00
0676b6c41f
Discord: Display channel on account creation form 2021-05-23 17:31:20 +01:00
ClankJake
ac842e6273 translation from Weblate (Portuguese (Brazil))
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/pt_BR/
2021-05-23 18:23:21 +02:00
ce8cdced4d
Discord: Fix GetUsers, add invite messages
The "Send to" box on the invite tab now accepts username#discriminator,
and a search icon has been added which opens a search window similar to
the one on the accounts tab. DiscordDaemon.GetUsers was also very broken
and wouldn't work with full username#discriminator, that's been fixed.
2021-05-23 16:16:31 +01:00
b8e3fc636c
Accounts: Fix cog on telegram when no discord linked
Also, disable telegram & discord if an auth/initialization error occurs.
2021-05-23 14:48:36 +01:00
519a5615cc
Accounts: Fix email check on dropdown 2021-05-23 14:35:01 +01:00
168b217553
Discord: fix user links 2021-05-23 14:32:35 +01:00
7d698d63e3
Discord: split discord search into own module
Will also be used for "Send to" on the invite page.
2021-05-23 14:22:18 +01:00
035dbde819
last image 2021-05-23 02:11:48 +01:00
c373d8b2d6
add final oauth tab image 2021-05-23 02:08:02 +01:00
8698c3c6a4
add oauth2 section to bot instructions 2021-05-23 02:03:55 +01:00
0edd2ba68b
add settings image for bot setup 2021-05-23 01:51:59 +01:00
b91f0b5a18
Discord: add images for bot creation instructions 2021-05-23 01:38:40 +01:00
24fa841c0d
Discord: Wait for non-nil pointer to bot data
While testing others things, I had quite a few nil pointer dereference
errors from accessing bot data right after initializing. A for loop now
waits until the first of the pointers is non-nil, which should
hopefully avoid crashes.
2021-05-23 01:09:03 +01:00
44558b8109
Discord: Remove extra newlines around links
Since links are converted into embeds, links put on their own line often
lead to extra newlines that looks pretty weird. They should now be
stripped.
2021-05-23 01:05:53 +01:00
e98c9b46f1
Accounts: no wrapping for contact dropdown 2021-05-22 23:18:43 +01:00
b3ce7acfcb
Accounts: Always inline icons, only one settings cog
Admin chip, email edit bot and contact method cog icon are now always inline.
Only one cog icon is shown now.
2021-05-22 23:05:53 +01:00
9fac79b1f0
Discord: Add users via accounts tab
Doesn't require a PIN like Telegram, as we can access a list of guild
users with the GuildMembers intent set. This has to be enabled under
Bot > Priviliged Gateway intents on the developer portal.
2021-05-22 21:42:15 +01:00
591e3c5ca1
Discord: embed images
![alt](image link) is now converted to an image embed.
2021-05-22 15:32:51 +01:00
35d407afef
Discord: remove @ from username 2021-05-22 15:31:25 +01:00
a6447165b7
add email notify enable/disable; remove (de)hyphening
hyphen/dehyphen conflicted with new migration for email contact
preference, and it's been a while since this has been an issue so i've
just commented it out for now.
2021-05-21 22:46:46 +01:00
3bf722c5fe
Discord: send links as embeds
Kind of janky but works. This kind of messes up the layout if you write
links in-line.
2021-05-21 21:59:50 +01:00
f8f5f35cc1
PIN verification, notifications, multiple notif providers
Discord, Email & Telegram can be enabled, although email is always
enabled right now (will fix). Also apparently markdown hyperlinks don't
work in Discord, eventually will implement something to convert them to
embeds.
2021-05-21 21:35:25 +01:00
524941da0c
fix heading size with sm 2021-05-21 21:10:32 +01:00
22bba922f9
Discord: Add !lang command 2021-05-18 18:41:42 +01:00
d928df7ab2
Discord: Start bot, add !start and pin validity check
The bot should be created by the admin and added to a discord server
mutual to the intended new user(s). On !start in the server,
communication is moved to DMs. Currently !start works, and validity of a
given PIN is checked although nothing it done with this yet.
2021-05-17 23:42:33 +01:00
40 changed files with 1803 additions and 401 deletions

View File

@ -52,6 +52,13 @@ 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
@ -91,7 +98,7 @@ compile:
$(GOBINARY) mod download $(GOBINARY) mod download
$(info Building) $(info Building)
mkdir -p build mkdir -p build
$(GOBINARY) build -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go $(GOBINARY) build $(RACEDETECTOR) -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 Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it. * Telegram & Discord Integration: Verify users via a Telegram or Discord bot, 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,6 +54,8 @@ 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.

264
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.(string) email = e.Addr
} }
for _, ombiUser := range ombiUsers { for _, ombiUser := range ombiUsers {
ombiAddr := "" ombiAddr := ""
@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
} }
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if emailEnabled { if emailEnabled {
app.storage.emails[id] = req.Email app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -330,6 +330,30 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false success = false
return return
} }
var discordUser DiscordUser
discordVerified := false
if discordEnabled {
if req.DiscordPIN == "" {
if app.config.Section("discord").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord verification not completed", req.Code)
respond(401, "errorDiscordVerification", gc)
}
success = false
return
}
} else {
discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
if !discordVerified {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
respond(401, "errorInvalidPIN", gc)
}
success = false
return
}
}
}
telegramTokenIndex := -1 telegramTokenIndex := -1
if telegramEnabled { if telegramEnabled {
if req.TelegramPIN == "" { if req.TelegramPIN == "" {
@ -454,7 +478,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
// if app.config.Section("password_resets").Key("enabled").MustBool(false) { // if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" { if req.Email != "" {
app.storage.emails[id] = req.Email app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -479,7 +503,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.err.Printf("Failed to store user duration: %v", err) app.err.Printf("Failed to store user duration: %v", err)
} }
} }
if discordEnabled && discordVerified {
discordUser.Contact = req.DiscordContact
if app.storage.discord == nil {
app.storage.discord = map[string]DiscordUser{}
}
app.storage.discord[user.ID] = discordUser
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
} else {
delete(app.discord.verifiedTokens, req.DiscordPIN)
}
}
if telegramEnabled && telegramTokenIndex != -1 { if telegramEnabled && telegramTokenIndex != -1 {
tgToken := app.telegram.verifiedTokens[telegramTokenIndex] tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
tgUser := TelegramUser{ tgUser := TelegramUser{
@ -494,8 +529,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.storage.telegram = map[string]TelegramUser{} app.storage.telegram = map[string]TelegramUser{}
} }
app.storage.telegram[user.ID] = tgUser app.storage.telegram[user.ID] = tgUser
err := app.storage.storeTelegramUsers() if err := app.storage.storeTelegramUsers(); err != nil {
if err != nil {
app.err.Printf("Failed to store Telegram users: %v", err) app.err.Printf("Failed to store Telegram users: %v", err)
} else { } else {
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1] app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
@ -503,7 +537,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
} }
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 { if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
name := app.getAddressOrName(user.ID) name := app.getAddressOrName(user.ID)
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name) app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false) msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
@ -792,18 +826,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.UserMinutes = req.UserMinutes invite.UserMinutes = req.UserMinutes
} }
invite.ValidTill = validTill invite.ValidTill = validTill
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
app.debug.Printf("%s: Sending invite email", inviteCode) addressValid := false
invite.Email = req.Email discord := ""
app.debug.Printf("%s: Sending invite message", inviteCode)
if discordEnabled && !strings.Contains(req.SendTo, "@") {
users := app.discord.GetUsers(req.SendTo)
if len(users) == 0 {
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
} else if len(users) > 1 {
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
} else {
invite.SendTo = req.SendTo
addressValid = true
discord = users[0].User.ID
}
} else if emailEnabled {
addressValid = true
invite.SendTo = req.SendTo
}
if addressValid {
msg, err := app.email.constructInvite(inviteCode, invite, app, false) msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if err != nil { if err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err) app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
} else if err := app.email.send(msg, req.Email); err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
} else { } else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email) var err error
if discord != "" {
err = app.discord.SendDM(msg, discord)
} else {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
} else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
}
}
} }
} }
if req.Profile != "" { if req.Profile != "" {
@ -867,15 +927,15 @@ func (app *appContext) GetInvites(gc *gin.Context) {
if inv.RemainingUses != 0 { if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses invite.RemainingUses = inv.RemainingUses
} }
if inv.Email != "" { if inv.SendTo != "" {
invite.Email = inv.Email invite.SendTo = inv.SendTo
} }
if len(inv.Notify) != 0 { if len(inv.Notify) != 0 {
var address string var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails() app.storage.loadEmails()
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil { if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
address = addr.(string) address = addr.Addr
} }
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
@ -1074,14 +1134,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
} }
var address string var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
var ok bool addr, ok := app.storage.emails[gc.GetString("jfId")]
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
if !ok { if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code) app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user email", gc) respond(500, "Missing user email", gc)
return return
} }
address = addr.Addr
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
} }
@ -1168,7 +1228,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.LastActive = jfUser.LastActivityDate.Unix() user.LastActive = jfUser.LastActivityDate.Unix()
} }
if email, ok := app.storage.emails[jfUser.ID]; ok { if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.(string) user.Email = email.Addr
user.NotifyThroughEmail = email.Contact
} }
expiry, ok := app.storage.users[jfUser.ID] expiry, ok := app.storage.users[jfUser.ID]
if ok { if ok {
@ -1178,6 +1239,11 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.Telegram = tgUser.Username user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact user.NotifyThroughTelegram = tgUser.Contact
} }
if dc, ok := app.storage.discord[jfUser.ID]; ok {
user.Discord = dc.Username + "#" + dc.Discriminator
user.DiscordID = dc.ID
user.NotifyThroughDiscord = dc.Contact
}
resp.UserList[i] = user resp.UserList[i] = user
i++ i++
} }
@ -1253,7 +1319,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
for _, jfUser := range users { for _, jfUser := range users {
id := jfUser.ID id := jfUser.ID
if address, ok := req[id]; ok { if address, ok := req[id]; ok {
app.storage.emails[id] = address contact := true
if oldAddr, ok := app.storage.emails[id]; ok {
contact = oldAddr.Contact
}
app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
if ombiEnabled { if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id) ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil { if code == 200 && err == nil {
@ -2004,38 +2074,63 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Summary Sets whether to notify a user through telegram or not. // @Summary Sets whether to notify a user through telegram or not.
// @Produce json // @Produce json
// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram." // @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse // @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse // @Success 500 {object} boolResponse
// @Router /users/telegram/notify [post] // @Router /users/telegram/notify [post]
// @Security Bearer // @Security Bearer
// @tags Other // @tags Other
func (app *appContext) TelegramSetNotify(gc *gin.Context) { func (app *appContext) SetContactMethods(gc *gin.Context) {
var req telegramNotifyDTO var req SetContactMethodsDTO
gc.BindJSON(&req) gc.BindJSON(&req)
if req.ID == "" { if req.ID == "" {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
} }
if tgUser, ok := app.storage.telegram[req.ID]; ok { if tgUser, ok := app.storage.telegram[req.ID]; ok {
tgUser.Contact = req.Enabled tgUser.Contact = req.Telegram
app.storage.telegram[req.ID] = tgUser app.storage.telegram[req.ID] = tgUser
if err := app.storage.storeTelegramUsers(); err != nil { if err := app.storage.storeTelegramUsers(); err != nil {
respondBool(500, false, gc) respondBool(500, false, gc)
app.err.Printf("Telegram: Failed to store users: %v", err) app.err.Printf("Telegram: Failed to store users: %v", err)
return return
} }
respondBool(200, true, gc)
msg := "" msg := ""
if !req.Enabled { if !req.Telegram {
msg = "not" msg = " not"
} }
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg) app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
}
if dcUser, ok := app.storage.discord[req.ID]; ok {
dcUser.Contact = req.Discord
app.storage.discord[req.ID] = dcUser
if err := app.storage.storeDiscordUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Discord: Failed to store users: %v", err)
return return
} }
app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID) msg := ""
respondBool(400, false, gc) if !req.Discord {
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
}
if email, ok := app.storage.emails[req.ID]; ok {
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
respondBool(200, true, gc)
} }
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth. // @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
@ -2092,6 +2187,107 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
respondBool(200, tokenIndex != -1, gc) respondBool(200, tokenIndex != -1, gc)
} }
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/verified/{pin} [get]
// @tags Other
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
pin := gc.Param("pin")
_, ok := app.discord.verifiedTokens[pin]
respondBool(200, ok, gc)
}
// @Summary Returns a 10-minute, one-use Discord server invite
// @Produce json
// @Success 200 {object} DiscordInviteDTO
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/invite [get]
// @tags Other
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
if app.discord.inviteChannelName == "" {
respondBool(400, false, gc)
return
}
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
if invURL == "" {
respondBool(500, false, gc)
return
}
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
}
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
// @Produce json
// @Success 200 {object} DiscordUsersDTO
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param username path string true "username to search."
// @Router /users/discord/{username} [get]
// @tags Other
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
name := gc.Param("username")
if name == "" {
respondBool(400, false, gc)
return
}
users := app.discord.GetUsers(name)
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
for i, u := range users {
resp.Users[i] = DiscordUserDTO{
Name: u.User.Username + "#" + u.User.Discriminator,
ID: u.User.ID,
AvatarURL: u.User.AvatarURL("32"),
}
}
gc.JSON(200, resp)
}
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
// @Router /users/discord [post]
// @tags Other
func (app *appContext) DiscordConnect(gc *gin.Context) {
var req DiscordConnectUserDTO
gc.BindJSON(&req)
if req.JellyfinID == "" || req.DiscordID == "" {
respondBool(400, false, gc)
return
}
user, ok := app.discord.NewUser(req.DiscordID)
if !ok {
respondBool(500, false, gc)
return
}
app.storage.discord[req.JellyfinID] = user
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Restarts the program. No response means success. // @Summary Restarts the program. No response means success.
// @Router /restart [post] // @Router /restart [post]
// @Security Bearer // @Security Bearer

View File

@ -14,6 +14,7 @@ import (
var emailEnabled = false var emailEnabled = false
var messagesEnabled = false var messagesEnabled = false
var telegramEnabled = false var telegramEnabled = false
var discordEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) { func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("") val := app.config.Section(sect).Key(key).MustString("")
@ -42,7 +43,7 @@ func (app *appContext) loadConfig() error {
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
} }
} }
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} { for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users"} {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
} }
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
@ -87,15 +88,17 @@ func (app *appContext) loadConfig() error {
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit)) app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false) messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false) telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
if !messagesEnabled { if !messagesEnabled {
emailEnabled = false emailEnabled = false
telegramEnabled = false telegramEnabled = false
discordEnabled = false
} else if app.config.Section("email").Key("method").MustString("") == "" { } else if app.config.Section("email").Key("method").MustString("") == "" {
emailEnabled = false emailEnabled = false
} else { } else {
emailEnabled = true emailEnabled = true
} }
if !emailEnabled && !telegramEnabled { if !emailEnabled && !telegramEnabled && !discordEnabled {
messagesEnabled = false messagesEnabled = false
} }
@ -168,3 +171,28 @@ func (app *appContext) migrateEmailConfig() {
} }
app.loadConfig() app.loadConfig()
} }
func (app *appContext) migrateEmailStorage() error {
var emails map[string]interface{}
err := loadJSON(app.storage.emails_path, &emails)
if err != nil {
return err
}
newEmails := map[string]EmailAddress{}
for jfID, addr := range emails {
newEmails[jfID] = EmailAddress{
Addr: addr.(string),
Contact: true,
}
}
err = storeJSON(app.storage.emails_path+".bak", emails)
if err != nil {
return err
}
err = storeJSON(app.storage.emails_path, newEmails)
if err != nil {
return err
}
app.info.Println("Migrated to new email format. A backup has also been made.")
return nil
}

View File

@ -546,6 +546,89 @@
} }
} }
}, },
"discord": {
"order": [],
"meta": {
"name": "Discord",
"description": "Settings for Discord invites/signup/notifications"
},
"settings": {
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
},
"required": {
"name": "Require on sign-up",
"required": false,
"required_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Require Discord connection on sign-up."
},
"token": {
"name": "API Token",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "Discord Bot API Token."
},
"start_command": {
"name": "Start command",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "!start",
"description": "Command to start the user verification process."
},
"channel": {
"name": "Channel to monitor",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "Only listen to commands in specified channel. Leave blank to monitor all."
},
"provide_invite": {
"name": "Provide server invite",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Generate a one-time discord server invite for the account creation form. Required Bot permission \"Create instant invite\", you may need to re-add the bot to your server after."
},
"invite_channel": {
"name": "Invite channel",
"required": false,
"requires_restart": true,
"depends_true": "provide_invite",
"type": "text",
"value": "",
"description": "Channel to invite new users to."
},
"language": {
"name": "Language",
"required": false,
"requires_restart": false,
"depends_true": "enabled",
"type": "select",
"options": [
["en-us", "English (US)"]
],
"value": "en-us",
"description": "Default Discord message language. Visit weblate if you'd like to translate."
}
}
},
"telegram": { "telegram": {
"order": [], "order": [],
"meta": { "meta": {
@ -565,6 +648,7 @@
"name": "Require on sign-up", "name": "Require on sign-up",
"required": false, "required": false,
"required_restart": true, "required_restart": true,
"depends_true": "enabled",
"type": "bool", "type": "bool",
"value": false, "value": false,
"description": "Require telegram connection on sign-up." "description": "Require telegram connection on sign-up."
@ -1140,6 +1224,14 @@
"type": "text", "type": "text",
"value": "", "value": "",
"description": "Stores telegram user IDs and language preferences." "description": "Stores telegram user IDs and language preferences."
},
"discord_users": {
"name": "Discord users",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Stores discord user IDs and language preferences."
} }
} }
} }

View File

@ -30,12 +30,12 @@
} }
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 1000px) {
:root { :root {
font-size: 0.9rem; font-size: 0.9rem;
} }
.table-responsive table { .table-responsive table {
min-width: 660px; min-width: 800px;
} }
} }
@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) {
text-align: center; text-align: center;
} }
.w-100 {
width: 100%;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }
@ -172,7 +176,7 @@ div.card:contains(section.banner.footer) {
} }
p.sm, p.sm,
span.sm { span.sm:not(.heading) {
font-size: 0.75rem; font-size: 0.75rem;
} }
@ -424,6 +428,7 @@ p.top {
.table-responsive { .table-responsive {
overflow-x: auto; overflow-x: auto;
font-size: 0.9rem;
} }
#notification-box { #notification-box {
@ -438,6 +443,10 @@ p.top {
margin-bottom: -0.5rem; margin-bottom: -0.5rem;
} }
.dropdown-display.lg {
white-space: nowrap;
}
pre { pre {
white-space: pre-wrap; /* css-3 */ white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
@ -483,3 +492,32 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
max-width: 15rem; max-width: 15rem;
min-width: 10rem; min-width: 10rem;
} }
td.img-circle {
width: 32px;
height: 32px;
}
span.img-circle.lg {
width: 64px;
height: 64px;
}
span.shield.img-circle {
padding: 0.2rem;
}
img.img-circle {
border-radius: 50%;
vertical-align: middle;
}
.table td.sm {
padding-top: 0.1rem;
padding-bottom: 0.1rem;
}
.table-inline {
display: flex !important;
align-items: center;
}

410
discord.go Normal file
View File

@ -0,0 +1,410 @@
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 { if telegramEnabled || discordEnabled {
keys = []string{"text"} keys = []string{"text"}
text, markdown = "", "" text, markdown = "", ""
} else { } else {
@ -238,7 +238,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
text = "" text = ""
} }
} else { } else {
if telegramEnabled { if telegramEnabled || discordEnabled {
keys = []string{"html", "text", "markdown"} keys = []string{"html", "text", "markdown"}
} else { } else {
keys = []string{"html", "text"} keys = []string{"html", "text"}
@ -807,8 +807,21 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
var err error var err error
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled { if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID) err = app.telegram.Send(email, tgChat.ChatID)
} else if address, ok := app.storage.emails[id]; ok { if err != nil {
err = app.email.send(email, address.(string)) return err
}
}
if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
err = app.discord.Send(email, dcChat.ChannelID)
if err != nil {
return err
}
}
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
err = app.email.send(email, address.Addr)
if err != nil {
return err
}
} }
if err != nil { if err != nil {
return err return err
@ -818,11 +831,14 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
} }
func (app *appContext) getAddressOrName(jfID string) string { func (app *appContext) getAddressOrName(jfID string) string {
if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
return dcChat.Username + "#" + dcChat.Discriminator
}
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled { if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
return "@" + tgChat.Username return "@" + tgChat.Username
} }
if addr, ok := app.storage.emails[jfID]; ok { if addr, ok := app.storage.emails[jfID]; ok {
return addr.(string) return addr.Addr
} }
return "" return ""
} }

1
go.mod
View File

@ -11,6 +11,7 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
replace github.com/hrfee/jfa-go/logger => ./logger replace github.com/hrfee/jfa-go/logger => ./logger
require ( require (
github.com/bwmarrin/discordgo v0.23.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a // indirect github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a // indirect

5
go.sum
View File

@ -11,6 +11,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
@ -148,6 +150,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I= github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U= github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs= github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
@ -265,6 +269,7 @@ github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -7,6 +7,7 @@
window.notificationsEnabled = {{ .notifications }}; window.notificationsEnabled = {{ .notifications }};
window.emailEnabled = {{ .email_enabled }}; window.emailEnabled = {{ .email_enabled }};
window.telegramEnabled = {{ .telegram_enabled }}; window.telegramEnabled = {{ .telegram_enabled }};
window.discordEnabled = {{ .discord_enabled }};
window.ombiEnabled = {{ .ombiEnabled }}; window.ombiEnabled = {{ .ombiEnabled }};
window.usernameEnabled = {{ .username }}; window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }}); window.langFile = JSON.parse({{ .language }});
@ -327,6 +328,18 @@
</div> </div>
</div> </div>
{{ end }} {{ end }}
{{ if .discord_enabled }}
<div id="modal-discord" class="modal">
<div class="modal-content card">
<span class="heading mb-1"><span id="discord-header"></span><span class="modal-close">&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">
@ -488,7 +501,14 @@
<div id="create-send-to-container"> <div id="create-send-to-container">
<label class="label supra">{{ .strings.inviteSendToEmail }}</label> <label class="label supra">{{ .strings.inviteSendToEmail }}</label>
<div class="flex-expand mb-1 mt-half"> <div class="flex-expand mb-1 mt-half">
{{ if .discord_enabled }}
<input type="text" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com | user#1234">
<span id="create-send-to-search" class="button ~neutral !normal mr-1">
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
</span>
{{ else }}
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com"> <input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
{{ end }}
<label for="create-send-to-enabled" class="button ~neutral !normal"> <label for="create-send-to-enabled" class="button ~neutral !normal">
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled"> <input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
</label> </label>
@ -525,6 +545,9 @@
{{ if .telegram_enabled }} {{ if .telegram_enabled }}
<th>Telegram</th> <th>Telegram</th>
{{ end }} {{ end }}
{{ if .discord_enabled }}
<th>Discord</th>
{{ end }}
<th>{{ .strings.expiry }}</th> <th>{{ .strings.expiry }}</th>
<th>{{ .strings.lastActiveTime }}</th> <th>{{ .strings.lastActiveTime }}</th>
</tr> </tr>

View File

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

View File

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

BIN
images/discord/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/discord/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
images/discord/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/discord/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/discord/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
images/discord/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
images/discord/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
images/discord/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -20,6 +20,8 @@
"create": "Create", "create": "Create",
"apply": "Apply", "apply": "Apply",
"delete": "Delete", "delete": "Delete",
"add": "Add",
"select": "Select",
"name": "Name", "name": "Name",
"date": "Date", "date": "Date",
"enabled": "Enabled", "enabled": "Enabled",
@ -94,7 +96,9 @@
"notifyEvent": "Notify on:", "notifyEvent": "Notify on:",
"notifyInviteExpiry": "On expiry", "notifyInviteExpiry": "On expiry",
"notifyUserCreation": "On user creation", "notifyUserCreation": "On user creation",
"sendPIN": "Ask the user to send the PIN below to the bot." "sendPIN": "Ask the user to send the PIN below to the bot.",
"searchDiscordUser": "Start typing the Discord username to find the user.",
"findDiscordUser": "Find Discord user"
}, },
"notifications": { "notifications": {
"changedEmailAddress": "Changed email address of {n}.", "changedEmailAddress": "Changed email address of {n}.",
@ -107,6 +111,7 @@
"updateApplied": "Update applied, please restart.", "updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.", "updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.", "telegramVerified": "Telegram account verified.",
"accountConnected": "Account connected.",
"errorConnection": "Couldn't connect to jfa-go.", "errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.", "error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.", "errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",

View File

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

View File

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

View File

@ -15,7 +15,7 @@
"passwordRequirementsHeader": "Requisitos da Senha", "passwordRequirementsHeader": "Requisitos da Senha",
"successHeader": "Sucesso!", "successHeader": "Sucesso!",
"successContinueButton": "Continuar", "successContinueButton": "Continuar",
"confirmationRequired": "Necessária confirmação de e-mail", "confirmationRequired": "Confirmação por 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 /lang, and set language with /lang <language code>." "languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>."
} }
} }

163
main.go
View File

@ -16,7 +16,6 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
@ -96,6 +95,7 @@ type appContext struct {
validator Validator validator Validator
email *Emailer email *Emailer
telegram *TelegramDaemon telegram *TelegramDaemon
discord *DiscordDaemon
info, debug, err logger.Logger info, debug, err logger.Logger
host string host string
port int port int
@ -320,6 +320,10 @@ func start(asDaemon, firstCall bool) {
app.storage.emails_path = app.config.Section("files").Key("emails").String() app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil { if err := app.storage.loadEmails(); err != nil {
app.err.Printf("Failed to load Emails: %v", err) app.err.Printf("Failed to load Emails: %v", err)
err := app.migrateEmailStorage()
if err != nil {
app.err.Printf("Failed to migrate Email storage: %v", err)
}
} }
app.storage.policy_path = app.config.Section("files").Key("user_template").String() app.storage.policy_path = app.config.Section("files").Key("user_template").String()
if err := app.storage.loadPolicy(); err != nil { if err := app.storage.loadPolicy(); err != nil {
@ -341,6 +345,10 @@ func start(asDaemon, firstCall bool) {
if err := app.storage.loadTelegramUsers(); err != nil { if err := app.storage.loadTelegramUsers(); err != nil {
app.err.Printf("Failed to load Telegram users: %v", err) app.err.Printf("Failed to load Telegram users: %v", err)
} }
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
if err := app.storage.loadDiscordUsers(); err != nil {
app.err.Printf("Failed to load Discord users: %v", err)
}
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
app.storage.loadProfiles() app.storage.loadProfiles()
@ -429,76 +437,76 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err) app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
} }
app.info.Printf("Authenticated with %s", server) app.info.Printf("Authenticated with %s", server)
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs. // /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
This checks if the version is equal or higher. */ // This checks if the version is equal or higher. */
checkVersion := func(version string) int { // checkVersion := func(version string) int {
numberStrings := strings.Split(version, ".") // numberStrings := strings.Split(version, ".")
n := 0 // n := 0
for _, s := range numberStrings { // for _, s := range numberStrings {
num, err := strconv.Atoi(s) // num, err := strconv.Atoi(s)
if err == nil { // if err == nil {
n += num // n += num
} // }
} // }
return n // return n
} // }
if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") { // if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
// Get users to check if server uses hyphenated userIDs // // Get users to check if server uses hyphenated userIDs
app.jf.GetUsers(false) // app.jf.GetUsers(false)
noHyphens := true // noHyphens := true
for id := range app.storage.emails { // for id := range app.storage.emails {
if strings.Contains(id, "-") { // if strings.Contains(id, "-") {
noHyphens = false // noHyphens = false
break // break
} // }
} // }
if noHyphens == app.jf.Hyphens { // if noHyphens == app.jf.Hyphens {
var newEmails map[string]interface{} // var newEmails map[string]interface{}
var newUsers map[string]time.Time // var newUsers map[string]time.Time
var status, status2 int // var status, status2 int
var err, err2 error // var err, err2 error
if app.jf.Hyphens { // if app.jf.Hyphens {
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match.")) // app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
time.Sleep(time.Second * time.Duration(3)) // time.Sleep(time.Second * time.Duration(3))
newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails) // newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users) // newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
} else { // } else {
app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified.")) // app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
time.Sleep(time.Second * time.Duration(3)) // time.Sleep(time.Second * time.Duration(3))
newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails) // newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users) // newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
} // }
if status != 200 || err != nil { // if status != 200 || err != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) // app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
app.err.Fatalf("Couldn't upgrade emails.json") // app.err.Fatalf("Couldn't upgrade emails.json")
} // }
if status2 != 200 || err2 != nil { // if status2 != 200 || err2 != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) // app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
app.err.Fatalf("Couldn't upgrade users.json") // app.err.Fatalf("Couldn't upgrade users.json")
} // }
emailBakFile := app.storage.emails_path + ".bak" // emailBakFile := app.storage.emails_path + ".bak"
usersBakFile := app.storage.users_path + ".bak" // usersBakFile := app.storage.users_path + ".bak"
err = storeJSON(emailBakFile, app.storage.emails) // err = storeJSON(emailBakFile, app.storage.emails)
err2 = storeJSON(usersBakFile, app.storage.users) // err2 = storeJSON(usersBakFile, app.storage.users)
if err != nil { // if err != nil {
app.err.Fatalf("couldn't store emails.json backup: %v", err) // app.err.Fatalf("couldn't store emails.json backup: %v", err)
} // }
if err2 != nil { // if err2 != nil {
app.err.Fatalf("couldn't store users.json backup: %v", err) // app.err.Fatalf("couldn't store users.json backup: %v", err)
} // }
app.storage.emails = newEmails // app.storage.emails = newEmails
app.storage.users = newUsers // app.storage.users = newUsers
err = app.storage.storeEmails() // err = app.storage.storeEmails()
err2 = app.storage.storeUsers() // err2 = app.storage.storeUsers()
if err != nil { // if err != nil {
app.err.Fatalf("couldn't store emails.json: %v", err) // app.err.Fatalf("couldn't store emails.json: %v", err)
} // }
if err2 != nil { // if err2 != nil {
app.err.Fatalf("couldn't store users.json: %v", err) // app.err.Fatalf("couldn't store users.json: %v", err)
} // }
} // }
} // }
// Auth (manual user/pass or jellyfin) // Auth (manual user/pass or jellyfin)
app.jellyfinLogin = true app.jellyfinLogin = true
@ -562,11 +570,22 @@ func start(asDaemon, firstCall bool) {
app.telegram, err = newTelegramDaemon(app) app.telegram, err = newTelegramDaemon(app)
if err != nil { if err != nil {
app.err.Printf("Failed to authenticate with Telegram: %v", err) app.err.Printf("Failed to authenticate with Telegram: %v", err)
telegramEnabled = false
} else { } else {
go app.telegram.run() go app.telegram.run()
defer app.telegram.Shutdown() defer app.telegram.Shutdown()
} }
} }
if discordEnabled {
app.discord, err = newDiscordDaemon(app)
if err != nil {
app.err.Printf("Failed to authenticate with Discord: %v", err)
discordEnabled = false
} else {
go app.discord.run()
defer app.discord.Shutdown()
}
}
} else { } else {
debugMode = false debugMode = false
address = "0.0.0.0:8056" address = "0.0.0.0:8056"
@ -602,12 +621,12 @@ func start(asDaemon, firstCall bool) {
app.err.Printf("Failure serving: %s", err) app.err.Printf("Failure serving: %s", err)
} }
} }
}()
if firstRun { if firstRun {
app.info.Printf("Loaded, visit %s to start.", address) app.info.Printf("Loaded, visit %s to start.", address)
} else { } else {
app.info.Printf("Loaded @ %s", address) 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() {
@ -760,7 +779,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 && flagPassed("tray") { } else if TRAY {
RunTray() RunTray()
} else { } else {
RESTART = make(chan bool, 1) RESTART = make(chan bool, 1)

View File

@ -17,6 +17,8 @@ type newUserDTO struct {
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser) Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used) TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
DiscordPIN string `json:"discord_pin" example:"A1-B2-3C"` // Discord verification PIN (if used)
DiscordContact bool `json:"discord_contact"` // Whether or not to use discord for notifications/pwrs
} }
type newUserResponse struct { type newUserResponse struct {
@ -48,7 +50,7 @@ type generateInviteDTO struct {
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
@ -98,7 +100,7 @@ type inviteDTO struct {
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable) RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable) SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable)
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
@ -125,12 +127,16 @@ type respUser struct {
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
Name string `json:"name" example:"jeff"` // Username of user Name string `json:"name" example:"jeff"` // Username of user
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available) Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
NotifyThroughEmail bool `json:"notify_email"`
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time. Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
Disabled bool `json:"disabled"` // Whether or not the user is disabled. Disabled bool `json:"disabled"` // Whether or not the user is disabled.
Telegram string `json:"telegram"` // Telegram username (if known) Telegram string `json:"telegram"` // Telegram username (if known)
NotifyThroughTelegram bool `json:"notify_telegram"` NotifyThroughTelegram bool `json:"notify_telegram"`
Discord string `json:"discord"` // Discord username (if known)
DiscordID string `json:"discord_id"` // Discord user ID for creating links.
NotifyThroughDiscord bool `json:"notify_discord"`
} }
type getUsersDTO struct { type getUsersDTO struct {
@ -249,7 +255,29 @@ type telegramSetDTO struct {
ID string `json:"id"` // Jellyfin ID of user. ID string `json:"id"` // Jellyfin ID of user.
} }
type telegramNotifyDTO struct { type SetContactMethodsDTO struct {
ID string `json:"id"` ID string `json:"id"`
Enabled bool `json:"enabled"` Email bool `json:"email"`
Discord bool `json:"discord"`
Telegram bool `json:"telegram"`
}
type DiscordUserDTO struct {
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
ID string `json:"id"`
}
type DiscordUsersDTO struct {
Users []DiscordUserDTO `json:"users"`
}
type DiscordConnectUserDTO struct {
JellyfinID string `json:"jf_id"`
DiscordID string `json:"discord_id"`
}
type DiscordInviteDTO struct {
InviteURL string `json:"invite"`
IconURL string `json:"icon"`
} }

View File

@ -121,6 +121,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if telegramEnabled { if telegramEnabled {
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite) router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
} }
if discordEnabled {
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
if app.config.Section("discord").Key("provide_invite").MustBool(false) {
router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite)
}
}
} }
if *SWAGGER { if *SWAGGER {
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
@ -158,11 +164,15 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig) api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)
if telegramEnabled { if telegramEnabled || discordEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin) api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified) api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser) api.POST(p+"/users/telegram", app.TelegramAddUser)
api.POST(p+"/users/telegram/notify", app.TelegramSetNotify) api.POST(p+"/users/contact", app.SetContactMethods)
}
if discordEnabled {
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
api.POST(p+"/users/discord", app.DiscordConnect)
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers) api.GET(p+"/ombi/users", app.OmbiUsers)

View File

@ -16,13 +16,15 @@ import (
type Storage struct { type Storage struct {
timePattern string timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path string
users map[string]time.Time users map[string]time.Time
invites Invites invites Invites
profiles map[string]Profile profiles map[string]Profile
defaultProfile string defaultProfile string
emails, displayprefs, ombi_template map[string]interface{} displayprefs, ombi_template map[string]interface{}
emails map[string]EmailAddress
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users. telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
customEmails customEmails customEmails customEmails
policy mediabrowser.Policy policy mediabrowser.Policy
configuration mediabrowser.Configuration configuration mediabrowser.Configuration
@ -37,6 +39,20 @@ type TelegramUser struct {
Contact bool // Whether to contact through telegram or not Contact bool // Whether to contact through telegram or not
} }
type DiscordUser struct {
ChannelID string
ID string
Username string
Discriminator string
Lang string
Contact bool
}
type EmailAddress struct {
Addr string
Contact bool
}
type customEmails struct { type customEmails struct {
UserCreated customEmail `json:"userCreated"` UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"` InviteExpiry customEmail `json:"inviteExpiry"`
@ -79,7 +95,7 @@ type Invite struct {
UserDays int `json:"user-days,omitempty"` UserDays int `json:"user-days,omitempty"`
UserHours int `json:"user-hours,omitempty"` UserHours int `json:"user-hours,omitempty"`
UserMinutes int `json:"user-minutes,omitempty"` UserMinutes int `json:"user-minutes,omitempty"`
Email string `json:"email"` SendTo string `json:"email"`
// Used to be stored as formatted time, now as Unix. // Used to be stored as formatted time, now as Unix.
UsedBy [][]string `json:"used-by"` UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"` Notify map[string]map[string]bool `json:"notify"`
@ -106,6 +122,7 @@ type Lang struct {
Common commonLangs Common commonLangs
SetupPath string SetupPath string
Setup setupLangs Setup setupLangs
// Telegram translations are also used for Discord bots (and likely future ones).
chosenTelegramLang string chosenTelegramLang string
TelegramPath string TelegramPath string
Telegram telegramLangs Telegram telegramLangs
@ -765,6 +782,14 @@ func (st *Storage) storeTelegramUsers() error {
return storeJSON(st.telegram_path, st.telegram) return storeJSON(st.telegram_path, st.telegram)
} }
func (st *Storage) loadDiscordUsers() error {
return loadJSON(st.discord_path, &st.discord)
}
func (st *Storage) storeDiscordUsers() error {
return storeJSON(st.discord_path, st.discord)
}
func (st *Storage) loadCustomEmails() error { func (st *Storage) loadCustomEmails() error {
return loadJSON(st.customEmails_path, &st.customEmails) return loadJSON(st.customEmails_path, &st.customEmails)
} }
@ -884,85 +909,85 @@ func storeJSON(path string, obj interface{}) error {
return err return err
} }
// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage. // // One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
//
func hyphenate(userID string) string { // func hyphenate(userID string) string {
if userID[8] == '-' { // if userID[8] == '-' {
return userID // return userID
} // }
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:] // return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
} // }
//
func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false) // jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil { // if status != 200 || err != nil {
return nil, status, err // return nil, status, err
} // }
newEmails := map[string]interface{}{} // newEmails := map[string]interface{}{}
for _, user := range jfUsers { // for _, user := range jfUsers {
unHyphenated := user.ID // unHyphenated := user.ID
hyphenated := hyphenate(unHyphenated) // hyphenated := hyphenate(unHyphenated)
val, ok := old[hyphenated] // val, ok := old[hyphenated]
if ok { // if ok {
newEmails[unHyphenated] = val // newEmails[unHyphenated] = val
} // }
} // }
return newEmails, status, err // return newEmails, status, err
} // }
//
func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false) // jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil { // if status != 200 || err != nil {
return nil, status, err // return nil, status, err
} // }
newEmails := map[string]interface{}{} // newEmails := map[string]interface{}{}
for _, user := range jfUsers { // for _, user := range jfUsers {
unstripped := user.ID // unstripped := user.ID
stripped := strings.ReplaceAll(unstripped, "-", "") // stripped := strings.ReplaceAll(unstripped, "-", "")
val, ok := old[stripped] // val, ok := old[stripped]
if ok { // if ok {
newEmails[unstripped] = val // newEmails[unstripped] = val
} // }
} // }
return newEmails, status, err // return newEmails, status, err
} // }
//
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
return app.hyphenateStorage(old) // return app.hyphenateStorage(old)
} // }
//
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
return app.deHyphenateStorage(old) // return app.deHyphenateStorage(old)
} // }
//
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) { // func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
asInterface := map[string]interface{}{} // asInterface := map[string]interface{}{}
for k, v := range old { // for k, v := range old {
asInterface[k] = v // asInterface[k] = v
} // }
fixed, status, err := app.hyphenateStorage(asInterface) // fixed, status, err := app.hyphenateStorage(asInterface)
if err != nil { // if err != nil {
return nil, status, err // return nil, status, err
} // }
out := map[string]time.Time{} // out := map[string]time.Time{}
for k, v := range fixed { // for k, v := range fixed {
out[k] = v.(time.Time) // out[k] = v.(time.Time)
} // }
return out, status, err // return out, status, err
} // }
//
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) { // func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
asInterface := map[string]interface{}{} // asInterface := map[string]interface{}{}
for k, v := range old { // for k, v := range old {
asInterface[k] = v // asInterface[k] = v
} // }
fixed, status, err := app.deHyphenateStorage(asInterface) // fixed, status, err := app.deHyphenateStorage(asInterface)
if err != nil { // if err != nil {
return nil, status, err // return nil, status, err
} // }
out := map[string]time.Time{} // out := map[string]time.Time{}
for k, v := range fixed { // for k, v := range fixed {
out[k] = v.(time.Time) // out[k] = v.(time.Time)
} // }
return out, status, err // return out, status, err
} // }

View File

@ -3,22 +3,57 @@ package main
import ( import (
"strings" "strings"
dg "github.com/bwmarrin/discordgo"
stripmd "github.com/writeas/go-strip-markdown" stripmd "github.com/writeas/go-strip-markdown"
) )
type Link struct {
Alt, URL string
}
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL. // StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
// Currently uses the deepest alt text when links/images are nested. // Currently uses the deepest alt text when links/images are nested.
func StripAltText(md string) string { // If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
altTextStart := -1 // Start of alt text (between '[' & ']') altTextStart := -1 // Start of alt text (between '[' & ']')
URLStart := -1 // Start of url (between '(' & ')') URLStart := -1 // Start of url (between '(' & ')')
URLEnd := -1 URLEnd := -1
previousURLEnd := -2 previousURLEnd := -2
out := "" out := ""
embeds := []*dg.MessageEmbed{}
for i := range md { for i := range md {
if altTextStart != -1 && URLStart != -1 && md[i] == ')' { if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
URLEnd = i - 1 URLEnd = i - 1
out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+1] out += md[previousURLEnd+2 : altTextStart-1]
if links {
embed := &dg.MessageEmbed{
Type: dg.EmbedTypeLink,
Title: md[altTextStart : URLStart-2],
}
if md[altTextStart-1] == '!' {
embed.Title = md[altTextStart+1 : URLStart-2]
embed.Type = dg.EmbedTypeImage
embed.Image = &dg.MessageEmbedImage{
URL: md[URLStart : URLEnd+1],
}
} else {
embed.URL = md[URLStart : URLEnd+1]
}
embeds = append(embeds, embed)
} else {
out += md[URLStart : URLEnd+1]
}
previousURLEnd = URLEnd previousURLEnd = URLEnd
// Removing links often leaves a load of extra newlines which look weird, this removes them.
if links {
next := 2
for md[URLEnd+next] == '\n' {
next++
}
if next >= 3 {
previousURLEnd += next - 2
}
}
altTextStart, URLStart, URLEnd = -1, -1, -1 altTextStart, URLStart, URLEnd = -1, -1, -1
continue continue
} }
@ -36,11 +71,12 @@ func StripAltText(md string) string {
out += md[previousURLEnd+2:] out += md[previousURLEnd+2:]
} }
if out == "" { if out == "" {
return md return md, embeds
} }
return out return out, embeds
} }
func stripMarkdown(md string) string { func stripMarkdown(md string) string {
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "</p>"), "<p>") stripped, _ := StripAltText(md, false)
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "</p>"), "<p>")
} }

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 VerifiedToken struct { type TelegramVerifiedToken struct {
Token string Token string
ChatID int64 ChatID int64
Username string Username string
@ -21,7 +21,7 @@ type TelegramDaemon struct {
bot *tg.BotAPI bot *tg.BotAPI
username string username string
tokens []string tokens []string
verifiedTokens []VerifiedToken verifiedTokens []TelegramVerifiedToken
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start. languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
link string link string
app *appContext app *appContext
@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return nil, err return nil, err
} }
td := &TelegramDaemon{ td := &TelegramDaemon{
Stopped: false,
ShutdownChannel: make(chan string), ShutdownChannel: make(chan string),
bot: bot, bot: bot,
username: bot.Self.UserName, username: bot.Self.UserName,
tokens: []string{}, tokens: []string{},
verifiedTokens: []VerifiedToken{}, verifiedTokens: []TelegramVerifiedToken{},
languages: map[int64]string{}, languages: map[int64]string{},
link: "https://t.me/" + bot.Self.UserName, link: "https://t.me/" + bot.Self.UserName,
app: app, app: app,
@ -55,10 +54,7 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return td, nil return td, nil
} }
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") func genAuthToken() string {
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (t *TelegramDaemon) NewAuthToken() string {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
pin := make([]rune, 8) pin := make([]rune, 8)
for i := range pin { for i := range pin {
@ -68,10 +64,18 @@ func (t *TelegramDaemon) NewAuthToken() string {
pin[i] = runes[rand.Intn(len(runes))] pin[i] = runes[rand.Intn(len(runes))]
} }
} }
t.tokens = append(t.tokens, string(pin))
return string(pin) return string(pin)
} }
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (t *TelegramDaemon) NewAuthToken() string {
pin := genAuthToken()
t.tokens = append(t.tokens, pin)
return pin
}
func (t *TelegramDaemon) run() { func (t *TelegramDaemon) run() {
t.app.info.Println("Starting Telegram bot daemon") t.app.info.Println("Starting Telegram bot daemon")
u := tg.NewUpdate(0) u := tg.NewUpdate(0)
@ -79,6 +83,7 @@ func (t *TelegramDaemon) run() {
updates, err := t.bot.GetUpdatesChan(u) updates, err := t.bot.GetUpdatesChan(u)
if err != nil { if err != nil {
t.app.err.Printf("Failed to start Telegram daemon: %v", err) t.app.err.Printf("Failed to start Telegram daemon: %v", err)
telegramEnabled = false
return return
} }
for { for {
@ -171,7 +176,7 @@ func (t *TelegramDaemon) Shutdown() {
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) { func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n" content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage") content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
err := t.Reply(upd, content) err := t.Reply(upd, content)
if err != nil { if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
@ -180,9 +185,9 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) { func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
if len(sects) == 1 { if len(sects) == 1 {
list := "/lang <lang>\n" list := "/lang `<lang>`\n"
for code := range t.app.storage.lang.Telegram { for code := range t.app.storage.lang.Telegram {
list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name) list += fmt.Sprintf("`%s`: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
} }
err := t.Reply(upd, list) err := t.Reply(upd, list)
if err != nil { if err != nil {
@ -196,8 +201,7 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
if user.ChatID == upd.Message.Chat.ID { if user.ChatID == upd.Message.Chat.ID {
user.Lang = sects[1] user.Lang = sects[1]
t.app.storage.telegram[jfID] = user t.app.storage.telegram[jfID] = user
err := t.app.storage.storeTelegramUsers() if err := t.app.storage.storeTelegramUsers(); err != nil {
if err != nil {
t.app.err.Printf("Failed to store Telegram users: %v", err) t.app.err.Printf("Failed to store Telegram users: %v", err)
} }
break break
@ -225,7 +229,7 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
if err != nil { if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
} }
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{ t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
Token: upd.Message.Text, Token: upd.Message.Text,
ChatID: upd.Message.Chat.ID, ChatID: upd.Message.Chat.ID,
Username: upd.Message.Chat.UserName, Username: upd.Message.Chat.UserName,

View File

@ -66,6 +66,10 @@ window.availableProfiles = window.availableProfiles || [];
if (window.telegramEnabled) { if (window.telegramEnabled) {
window.modals.telegram = new Modal(document.getElementById("modal-telegram")); window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
} }
if (window.discordEnabled) {
window.modals.discord = new Modal(document.getElementById("modal-discord"));
}
})(); })();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();

View File

@ -8,12 +8,18 @@ interface formWindow extends Window {
invalidPassword: string; invalidPassword: string;
successModal: Modal; successModal: Modal;
telegramModal: Modal; telegramModal: Modal;
discordModal: Modal;
confirmationModal: Modal confirmationModal: Modal
code: string; code: string;
messages: { [key: string]: string }; messages: { [key: string]: string };
confirmation: boolean; confirmation: boolean;
telegramRequired: boolean; telegramRequired: boolean;
telegramPIN: string; telegramPIN: string;
discordRequired: boolean;
discordPIN: string;
discordStartCommand: string;
discordInviteLink: boolean;
discordServerName: string;
userExpiryEnabled: boolean; userExpiryEnabled: boolean;
userExpiryMonths: number; userExpiryMonths: number;
userExpiryDays: number; userExpiryDays: number;
@ -68,7 +74,7 @@ if (window.telegramEnabled) {
telegramVerified = true; telegramVerified = true;
waiting.classList.add("~positive"); waiting.classList.add("~positive");
waiting.classList.remove("~info"); waiting.classList.remove("~info");
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]); window.notifications.customPositive("telegramVerified", "", window.messages["verified"]);
setTimeout(window.telegramModal.close, 2000); setTimeout(window.telegramModal.close, 2000);
telegramButton.classList.add("unfocused"); telegramButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused"); document.getElementById("contact-via").classList.remove("unfocused");
@ -84,6 +90,66 @@ if (window.telegramEnabled) {
}; };
} }
interface DiscordInvite {
invite: string;
icon: string;
}
var discordVerified = false;
if (window.discordEnabled) {
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
if (window.discordInviteLink) {
_get("/invite/" + window.code + "/discord/invite", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
return;
}
const inv = req.response as DiscordInvite;
const link = document.getElementById("discord-invite") as HTMLAnchorElement;
link.classList.add("subheading", "link-center");
link.href = inv.invite;
link.target = "_blank";
link.innerHTML = `<span class="img-circle lg mr-1"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`;
}
});
}
discordButton.onclick = () => {
const waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
toggleLoader(waiting);
window.discordModal.show();
let modalClosed = false;
window.discordModal.onclose = () => {
modalClosed = true;
toggleLoader(waiting);
}
const checkVerified = () => _get("/invite/" + window.code + "/discord/verified/" + window.discordPIN, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 401) {
window.discordModal.close();
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
return;
} else if (req.status == 200) {
if (req.response["success"] as boolean) {
discordVerified = true;
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customPositive("discordVerified", "", window.messages["verified"]);
setTimeout(window.discordModal.close, 2000);
discordButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
radio.checked = true;
} else if (!modalClosed) {
setTimeout(checkVerified, 1500);
}
}
}
});
checkVerified();
};
}
if (window.confirmation) { if (window.confirmation) {
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true); window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
} }
@ -161,6 +227,8 @@ interface sendDTO {
password: string; password: string;
telegram_pin?: string; telegram_pin?: string;
telegram_contact?: boolean; telegram_contact?: boolean;
discord_pin?: string;
discord_contact?: boolean;
} }
const create = (event: SubmitEvent) => { const create = (event: SubmitEvent) => {
@ -179,6 +247,13 @@ const create = (event: SubmitEvent) => {
send.telegram_contact = true; send.telegram_contact = true;
} }
} }
if (discordVerified) {
send.discord_pin = window.discordPIN;
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
if (radio.checked) {
send.discord_contact = true;
}
}
_post("/newUser", send, (req: XMLHttpRequest) => { _post("/newUser", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
let vals = req.response as respDTO; let vals = req.response as respDTO;

View File

@ -2,17 +2,22 @@ import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateStri
import { templateEmail } from "../modules/settings.js"; import { templateEmail } from "../modules/settings.js";
import { Marked } from "@ts-stack/markdown"; import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js"; import { stripMarkdown } from "../modules/stripmd.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
interface User { interface User {
id: string; id: string;
name: string; name: string;
email: string | undefined; email: string | undefined;
notify_email: boolean;
last_active: number; last_active: number;
admin: boolean; admin: boolean;
disabled: boolean; disabled: boolean;
expiry: number; expiry: number;
telegram: string; telegram: string;
notify_telegram: boolean; notify_telegram: boolean;
discord: string;
notify_discord: boolean;
discord_id: string;
} }
interface getPinResponse { interface getPinResponse {
@ -20,6 +25,8 @@ interface getPinResponse {
username: string; username: string;
} }
var addDiscord: (passData: string) => void;
class user implements User { class user implements User {
private _row: HTMLTableRowElement; private _row: HTMLTableRowElement;
private _check: HTMLInputElement; private _check: HTMLInputElement;
@ -27,16 +34,21 @@ class user implements User {
private _admin: HTMLSpanElement; private _admin: HTMLSpanElement;
private _disabled: HTMLSpanElement; private _disabled: HTMLSpanElement;
private _email: HTMLInputElement; private _email: HTMLInputElement;
private _notifyEmail: boolean;
private _emailAddress: string; private _emailAddress: string;
private _emailEditButton: HTMLElement; private _emailEditButton: HTMLElement;
private _telegram: HTMLTableDataCellElement; private _telegram: HTMLTableDataCellElement;
private _telegramUsername: string; private _telegramUsername: string;
private _notifyTelegram: boolean; private _notifyTelegram: boolean;
private _discord: HTMLTableDataCellElement;
private _discordUsername: string;
private _discordID: string;
private _notifyDiscord: boolean;
private _expiry: HTMLTableDataCellElement; private _expiry: HTMLTableDataCellElement;
private _expiryUnix: number; private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement; private _lastActive: HTMLTableDataCellElement;
private _lastActiveUnix: number; private _lastActiveUnix: number;
id: string; id = "";
private _selected: boolean; private _selected: boolean;
get selected(): boolean { return this._selected; } get selected(): boolean { return this._selected; }
@ -82,6 +94,21 @@ class user implements User {
} }
} }
get notify_email(): boolean { return this._notifyEmail; }
set notify_email(s: boolean) {
this._notifyEmail = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
if (email) {
email.checked = s;
}
}
if (window.discordEnabled && this._discordUsername) {
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
email.checked = s;
}
}
get telegram(): string { return this._telegramUsername; } get telegram(): string { return this._telegramUsername; }
set telegram(u: string) { set telegram(u: string) {
if (!window.telegramEnabled) return; if (!window.telegramEnabled) return;
@ -90,31 +117,39 @@ class user implements User {
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`; this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram; (this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else { } else {
this._telegram.innerHTML = ` let innerHTML = `
<div class="table-inline">
<a href="https://t.me/${u}" target="_blank">@${u}</a> <a href="https://t.me/${u}" target="_blank">@${u}</a>
`;
if (!window.discordEnabled || !this._discordUsername) {
innerHTML += `
<i class="icon ri-settings-2-line ml-half dropdown-button"></i> <i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual"> <div class="dropdown manual">
<div class="dropdown-display"> <div class="dropdown-display lg">
<div class="card ~neutral !low"> <div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span> <span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="switch pb-1 mt-half"> <label class="row switch pb-1 mt-half">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email"> <input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span> <span>Email</span>
</label> </label>
<label class="switch pb-1"> <label class="row switch pb-1">
<input type="radio" name="accounts-contact-${this.id}"> <input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span> <span>Telegram</span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
`; `;
}
innerHTML += "</div>";
this._telegram.innerHTML = innerHTML;
if (!window.discordEnabled || !this._discordUsername) {
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username. // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
const button = this._telegram.querySelector("i"); const button = this._telegram.querySelector("i");
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement; const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>; const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < radios.length; i++) { for (let i = 0; i < checks.length; i++) {
radios[i].onclick = this._setTelegramNotify; checks[i].onclick = () => this._setNotifyMethod("telegram");
} }
button.onclick = () => { button.onclick = () => {
@ -129,42 +164,140 @@ class user implements User {
}; };
} }
} }
}
get notify_telegram(): boolean { return this._notifyTelegram; } get notify_telegram(): boolean { return this._notifyTelegram; }
set notify_telegram(s: boolean) { set notify_telegram(s: boolean) {
if (!window.telegramEnabled || !this._telegramUsername) return; if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s; this._notifyTelegram = s;
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>; const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
radios[0].checked = !s; if (telegram) {
radios[1].checked = s; telegram.checked = s;
}
if (window.discordEnabled && this._discordUsername) {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s;
}
} }
private _setTelegramNotify = () => { private _setNotifyMethod = (mode: string = "telegram") => {
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>; let el: HTMLElement;
if (mode == "telegram") { el = this._telegram }
else if (mode == "discord") { el = this._discord }
const email = el.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
let send = { let send = {
id: this.id, id: this.id,
enabled: radios[1].checked email: email.checked
}; }
_post("/users/telegram/notify", send, (req: XMLHttpRequest) => { if (window.telegramEnabled && this._telegramUsername) {
const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
send["telegram"] = telegram.checked;
}
if (window.discordEnabled && this._discordUsername) {
const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
send["discord"] = discord.checked;
}
_post("/users/contact", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status != 200) { if (req.status != 200) {
window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings")); window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings"));
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked; document.dispatchEvent(new CustomEvent("accounts-reload"));
return; return;
} }
} }
}, false, (req: XMLHttpRequest) => { }, false, (req: XMLHttpRequest) => {
if (req.status == 0) { if (req.status == 0) {
window.notifications.connectionError(); window.notifications.connectionError();
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked; document.dispatchEvent(new CustomEvent("accounts-reload"));
return; return;
} else if (req.status == 401) { } else if (req.status == 401) {
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
document.dispatchEvent(new CustomEvent("accounts-reload"));
} }
}); });
} }
get discord(): string { return this._discordUsername; }
set discord(u: string) {
if (!window.discordEnabled) return;
this._discordUsername = u;
if (u == "") {
this._discord.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._discord.querySelector("span") as HTMLSpanElement).onclick = () => addDiscord(this.id);
} else {
let innerHTML = `
<div class="table-inline">
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown-display lg">
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="row switch pb-1 mt-half">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<span>Discord</span>
</label>
`;
if (window.telegramEnabled && this._telegramUsername != "") {
innerHTML += `
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
`;
}
innerHTML += `
</div>
</div>
</div>
</div>
`;
this._discord.innerHTML = innerHTML;
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
const button = this._discord.querySelector("i");
const dropdown = this._discord.querySelector("div.dropdown") as HTMLDivElement;
const checks = this._discord.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod("discord");
}
button.onclick = () => {
dropdown.classList.add("selected");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._discord.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
}
}
get discord_id(): string { return this._discordID; }
set discord_id(id: string) {
if (!window.discordEnabled || this._discordUsername == "") return;
this._discordID = id;
const link = this._discord.getElementsByClassName("discord-link")[0] as HTMLAnchorElement;
link.href = `https://discord.com/users/${id}`;
}
get notify_discord(): boolean { return this._notifyDiscord; }
set notify_discord(s: boolean) {
if (!window.discordEnabled || !this._discordUsername) return;
this._notifyDiscord = s;
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
discord.checked = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
discord.checked = s;
}
}
get expiry(): number { return this._expiryUnix; } get expiry(): number { return this._expiryUnix; }
set expiry(unix: number) { set expiry(unix: number) {
this._expiryUnix = unix; this._expiryUnix = unix;
@ -192,14 +325,19 @@ class user implements User {
this._row = document.createElement("tr") as HTMLTableRowElement; this._row = document.createElement("tr") as HTMLTableRowElement;
let innerHTML = ` let innerHTML = `
<td><input type="checkbox" value=""></td> <td><input type="checkbox" value=""></td>
<td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td> <td><div class="table-inline"><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></td>
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td> <td><div class="table-inline"><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></div></td>
`; `;
if (window.telegramEnabled) { if (window.telegramEnabled) {
innerHTML += ` innerHTML += `
<td class="accounts-telegram"></td> <td class="accounts-telegram"></td>
`; `;
} }
if (window.discordEnabled) {
innerHTML += `
<td class="accounts-discord"></td>
`;
}
innerHTML += ` innerHTML += `
<td class="accounts-expiry"></td> <td class="accounts-expiry"></td>
<td class="accounts-last-active"></td> <td class="accounts-last-active"></td>
@ -213,6 +351,7 @@ class user implements User {
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement; this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement; this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement; this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement; this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement; this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._check.onchange = () => { this.selected = this._check.checked; } this._check.onchange = () => { this.selected = this._check.checked; }
@ -319,12 +458,16 @@ class user implements User {
this.id = user.id; this.id = user.id;
this.name = user.name; this.name = user.name;
this.email = user.email || ""; this.email = user.email || "";
this.discord = user.discord;
this.telegram = user.telegram; this.telegram = user.telegram;
this.last_active = user.last_active; this.last_active = user.last_active;
this.admin = user.admin; this.admin = user.admin;
this.disabled = user.disabled; this.disabled = user.disabled;
this.expiry = user.expiry; this.expiry = user.expiry;
this.notify_discord = user.notify_discord;
this.notify_telegram = user.notify_telegram; this.notify_telegram = user.notify_telegram;
this.notify_email = user.notify_email;
this.discord_id = user.discord_id;
} }
asElement = (): HTMLTableRowElement => { return this._row; } asElement = (): HTMLTableRowElement => { return this._row; }
@ -935,6 +1078,19 @@ export class accountsList {
}; };
this._announceTextarea.onkeyup = this.loadPreview; this._announceTextarea.onkeyup = this.loadPreview;
addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => {
_post("/users/discord", {jf_id: id, discord_id: user.id}, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
document.dispatchEvent(new CustomEvent("accounts-reload"));
if (req.status != 200) {
window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs"));
return
}
window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected"));
window.modals.discord.close()
}
});
});
} }
reload = () => _get("/users", null, (req: XMLHttpRequest) => { reload = () => _get("/users", null, (req: XMLHttpRequest) => {

79
ts/modules/discord.ts Normal file
View File

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

View File

@ -1,4 +1,5 @@
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js"; import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
class DOMInvite implements Invite { class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => { updateNotify = (checkbox: HTMLInputElement) => {
@ -25,6 +26,7 @@ class DOMInvite implements Invite {
document.dispatchEvent(inviteDeletedEvent); document.dispatchEvent(inviteDeletedEvent);
} }
}) })
private _label: string = ""; private _label: string = "";
get label(): string { return this._label; } get label(): string { return this._label; }
set label(label: string) { set label(label: string) {
@ -82,10 +84,10 @@ class DOMInvite implements Invite {
this._middle.querySelector("strong.inv-remaining").textContent = remaining; this._middle.querySelector("strong.inv-remaining").textContent = remaining;
} }
private _email: string = ""; private _send_to: string = "";
get email(): string { return this._email }; get send_to(): string { return this._send_to };
set email(address: string) { set send_to(address: string) {
this._email = address; this._send_to = address;
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement; const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
const icon = container.querySelector("i"); const icon = container.querySelector("i");
const chip = container.querySelector("span.inv-email-chip"); const chip = container.querySelector("span.inv-email-chip");
@ -100,7 +102,7 @@ class DOMInvite implements Invite {
} else { } else {
container.classList.add("mr-1"); container.classList.add("mr-1");
chip.classList.add("chip"); chip.classList.add("chip");
if (address.includes("Failed to send to")) { if (address.includes("Failed")) {
icon.classList.remove("ri-mail-line"); icon.classList.remove("ri-mail-line");
icon.classList.add("ri-mail-close-line"); icon.classList.add("ri-mail-close-line");
chip.classList.remove("~neutral"); chip.classList.remove("~neutral");
@ -372,7 +374,7 @@ class DOMInvite implements Invite {
update = (invite: Invite) => { update = (invite: Invite) => {
this.code = invite.code; this.code = invite.code;
this.created = invite.created; this.created = invite.created;
this.email = invite.email; this.send_to = invite.send_to;
this.expiresIn = invite.expiresIn; this.expiresIn = invite.expiresIn;
if (window.notificationsEnabled) { if (window.notificationsEnabled) {
this.notifyCreation = invite.notifyCreation; this.notifyCreation = invite.notifyCreation;
@ -482,7 +484,7 @@ export class inviteList implements inviteList {
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite { function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
let parsed: Invite = {}; let parsed: Invite = {};
parsed.code = invite["code"] as string; parsed.code = invite["code"] as string;
parsed.email = invite["email"] as string || ""; parsed.send_to = invite["send_to"] as string || "";
parsed.label = invite["label"] as string || ""; parsed.label = invite["label"] as string || "";
let time = ""; let time = "";
let userExpiryTime = ""; let userExpiryTime = "";
@ -520,6 +522,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
export class createInvite { export class createInvite {
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement; private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement; private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
private _discordSearch: HTMLSpanElement;
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement; private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
private _uses = document.getElementById('create-uses') as HTMLInputElement; private _uses = document.getElementById('create-uses') as HTMLInputElement;
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement; private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
@ -542,6 +545,8 @@ export class createInvite {
private _invDuration = document.getElementById('inv-duration'); private _invDuration = document.getElementById('inv-duration');
private _userExpiry = document.getElementById('user-expiry'); private _userExpiry = document.getElementById('user-expiry');
private _sendToDiscord: (passData: string) => void;
// Broadcast when new invite created // Broadcast when new invite created
private _newInviteEvent = new CustomEvent("newInviteEvent"); private _newInviteEvent = new CustomEvent("newInviteEvent");
private _firstLoad = true; private _firstLoad = true;
@ -576,9 +581,19 @@ export class createInvite {
if (state) { if (state) {
this._sendToEnabled.parentElement.classList.remove("~neutral"); this._sendToEnabled.parentElement.classList.remove("~neutral");
this._sendToEnabled.parentElement.classList.add("~urge"); this._sendToEnabled.parentElement.classList.add("~urge");
if (window.discordEnabled) {
this._discordSearch.classList.remove("~neutral");
this._discordSearch.classList.add("~urge");
this._discordSearch.onclick = () => this._sendToDiscord("");
}
} else { } else {
this._sendToEnabled.parentElement.classList.remove("~urge"); this._sendToEnabled.parentElement.classList.remove("~urge");
this._sendToEnabled.parentElement.classList.add("~neutral"); this._sendToEnabled.parentElement.classList.add("~neutral");
if (window.discordEnabled) {
this._discordSearch.classList.remove("~urge");
this._discordSearch.classList.add("~neutral");
this._discordSearch.onclick = null;
}
} }
} }
@ -732,7 +747,7 @@ export class createInvite {
"multiple-uses": (this.uses > 1 || this.infiniteUses), "multiple-uses": (this.uses > 1 || this.infiniteUses),
"no-limit": this.infiniteUses, "no-limit": this.infiniteUses,
"remaining-uses": this.uses, "remaining-uses": this.uses,
"email": this.sendToEnabled ? this.sendTo : "", "send-to": this.sendToEnabled ? this.sendTo : "",
"profile": this.profile, "profile": this.profile,
"label": this.label "label": this.label
}; };
@ -761,7 +776,6 @@ export class createInvite {
this._userDays.disabled = true; this._userDays.disabled = true;
this._userHours.disabled = true; this._userHours.disabled = true;
this._userMinutes.disabled = true; this._userMinutes.disabled = true;
this.sendToEnabled = false;
this._createButton.onclick = this.create; this._createButton.onclick = this.create;
this.sendTo = ""; this.sendTo = "";
this.uses = 1; this.uses = 1;
@ -798,11 +812,22 @@ export class createInvite {
this._minutes.onchange = this._checkDurationValidity; this._minutes.onchange = this._checkDurationValidity;
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
if (!window.emailEnabled) { if (!window.emailEnabled && !window.discordEnabled) {
document.getElementById("create-send-to-container").classList.add("unfocused"); document.getElementById("create-send-to-container").classList.add("unfocused");
} }
if (window.discordEnabled) {
this._discordSearch = document.getElementById("create-send-to-search") as HTMLSpanElement;
this._sendToDiscord = newDiscordSearch(
window.lang.strings("findDiscordUser"),
window.lang.strings("searchDiscordUser"),
window.lang.strings("select"),
(user: DiscordUser) => {
this.sendTo = user.name;
window.modals.discord.close();
}
);
}
this.sendToEnabled = false;
} }
} }

View File

@ -21,6 +21,7 @@ declare interface Window {
notificationsEnabled: boolean; notificationsEnabled: boolean;
emailEnabled: boolean; emailEnabled: boolean;
telegramEnabled: boolean; telegramEnabled: boolean;
discordEnabled: boolean;
ombiEnabled: boolean; ombiEnabled: boolean;
usernameEnabled: boolean; usernameEnabled: boolean;
token: string; token: string;
@ -101,13 +102,14 @@ declare interface Modals {
extendExpiry: Modal; extendExpiry: Modal;
updateInfo: Modal; updateInfo: Modal;
telegram: Modal; telegram: Modal;
discord: Modal;
} }
interface Invite { interface Invite {
code?: string; code?: string;
expiresIn?: string; expiresIn?: string;
remainingUses?: string; remainingUses?: string;
email?: string; send_to?: string;
usedBy?: { [name: string]: number }; usedBy?: { [name: string]: number };
created?: number; created?: number;
notifyExpiry?: boolean; notifyExpiry?: boolean;

View File

@ -1,8 +1,8 @@
package main package main
import ( import (
"archive/tar" "archive/zip"
"compress/gzip" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -184,6 +184,7 @@ 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()
@ -347,7 +348,11 @@ func getBuildName() string {
if arch == "" { if arch == "" {
return "" return ""
} }
return operatingSystem + "_" + arch tray := ""
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) {
@ -392,27 +397,20 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
gz, err := gzip.NewReader(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
status = -1 status = -1
return return
} }
defer gz.Close() zp, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
tarReader := tar.NewReader(gz)
var header *tar.Header
for {
header, err = tarReader.Next()
if err == io.EOF {
break
}
if err != nil { if err != nil {
status = -1 status = -1
return return
} }
switch header.Typeflag { for _, zf := range zp.File {
case tar.TypeReg: if zf.Name != ud.binary {
// Search only for file named ud.binary continue
if header.Name == ud.binary { }
var file string var file string
file, err = os.Executable() file, err = os.Executable()
if err != nil { if err != nil {
@ -429,13 +427,19 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
return return
} }
mode := info.Mode() mode := info.Mode()
var unzippedFile io.ReadCloser
unzippedFile, err = zf.Open()
if err != nil {
return
}
defer unzippedFile.Close()
var f *os.File var f *os.File
f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
if err != nil { if err != nil {
return return
} }
defer f.Close() defer f.Close()
_, err = io.Copy(f, tarReader) _, err = io.Copy(f, unzippedFile)
if err != nil { if err != nil {
return return
} }
@ -453,8 +457,69 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
} }
return 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,6 +1,7 @@
package main package main
import ( import (
"html/template"
"io/fs" "io/fs"
"net/http" "net/http"
"strconv" "strconv"
@ -121,6 +122,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"contactMessage": "", "contactMessage": "",
"email_enabled": emailEnabled, "email_enabled": emailEnabled,
"telegram_enabled": telegramEnabled, "telegram_enabled": telegramEnabled,
"discord_enabled": discordEnabled,
"notifications": notificationsEnabled, "notifications": notificationsEnabled,
"version": version, "version": version,
"commit": commit, "commit": commit,
@ -256,8 +258,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
} }
return return
} }
email := app.storage.invites[code].Email email := app.storage.invites[code].SendTo
if strings.Contains(email, "Failed") { if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = "" email = ""
} }
data := gin.H{ data := gin.H{
@ -284,13 +286,30 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"), "userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang, "langName": lang,
"telegramEnabled": telegramEnabled, "telegramEnabled": telegramEnabled,
"discordEnabled": discordEnabled,
} }
if data["telegramEnabled"].(bool) { if telegramEnabled {
data["telegramPIN"] = app.telegram.NewAuthToken() data["telegramPIN"] = app.telegram.NewAuthToken()
data["telegramUsername"] = app.telegram.username data["telegramUsername"] = app.telegram.username
data["telegramURL"] = app.telegram.link data["telegramURL"] = app.telegram.link
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false) data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
} }
if discordEnabled {
data["discordPIN"] = app.discord.NewAuthToken()
data["discordUsername"] = app.discord.username
data["discordRequired"] = app.config.Section("discord").Key("required").MustBool(false)
data["discordSendPINMessage"] = template.HTML(app.storage.lang.Form[lang].Strings.template("sendPINDiscord", tmpl{
"command": `<code class="code">` + app.config.Section("discord").Key("start_command").MustString("!start") + `</code>`,
"server_channel": app.discord.serverChannelName,
}))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
// if discordEnabled {
// pin := ""
// for _, token := range app.discord.tokens {
// if
gcHTML(gc, http.StatusOK, "form-loader.html", data) gcHTML(gc, http.StatusOK, "form-loader.html", data)
} }