mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
commit
fe5cf69b7a
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,3 +14,4 @@ server.pem
|
||||
server.crt
|
||||
instructions-debian.txt
|
||||
cl.md
|
||||
./telegram/
|
||||
|
@ -17,11 +17,12 @@ 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.
|
||||
* 🔗 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.
|
||||
* Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
|
||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email 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.
|
||||
* 🔑 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.
|
||||
* Notifications: Get notified when someone creates an account, or an invite expires.
|
||||
* 📣 Announcements: Bulk email your users with announcements about your server.
|
||||
* 📣 Announcements: Bulk message your users with announcements about your server.
|
||||
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
||||
* Enables the usage of jfa-go by multiple people
|
||||
* 🌓 Customizations
|
||||
|
351
api.go
351
api.go
@ -39,9 +39,9 @@ func respondBool(code int, val bool, gc *gin.Context) {
|
||||
}
|
||||
|
||||
func (app *appContext) loadStrftime() {
|
||||
app.datePattern = app.config.Section("email").Key("date_format").String()
|
||||
app.datePattern = app.config.Section("messages").Key("date_format").String()
|
||||
app.timePattern = `%H:%M`
|
||||
if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val {
|
||||
if val, _ := app.config.Section("messages").Key("use_24h").Bool(); !val {
|
||||
app.timePattern = `%I:%M %p`
|
||||
}
|
||||
return
|
||||
@ -282,7 +282,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
if emailEnabled {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
@ -330,15 +330,44 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
return
|
||||
}
|
||||
telegramTokenIndex := -1
|
||||
if telegramEnabled {
|
||||
if req.TelegramPIN == "" {
|
||||
if app.config.Section("telegram").Key("required").MustBool(false) {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Telegram verification not completed", req.Code)
|
||||
respond(401, "errorTelegramVerification", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
if v.Token == req.TelegramPIN {
|
||||
telegramTokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if telegramTokenIndex == -1 {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code)
|
||||
respond(401, "errorInvalidPIN", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
|
||||
claims := jwt.MapClaims{
|
||||
"valid": true,
|
||||
"invite": req.Code,
|
||||
"email": req.Email,
|
||||
"username": req.Username,
|
||||
"password": req.Password,
|
||||
"exp": strconv.FormatInt(time.Now().Add(time.Hour*12).Unix(), 10),
|
||||
"type": "confirmation",
|
||||
"valid": true,
|
||||
"invite": req.Code,
|
||||
"email": req.Email,
|
||||
"username": req.Username,
|
||||
"password": req.Password,
|
||||
"telegramPIN": req.TelegramPIN,
|
||||
"exp": strconv.FormatInt(time.Now().Add(time.Hour*12).Unix(), 10),
|
||||
"type": "confirmation",
|
||||
}
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
@ -450,15 +479,40 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
app.err.Printf("Failed to store user duration: %v", err)
|
||||
}
|
||||
}
|
||||
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
|
||||
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
|
||||
|
||||
if telegramEnabled && telegramTokenIndex != -1 {
|
||||
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
|
||||
tgUser := TelegramUser{
|
||||
ChatID: tgToken.ChatID,
|
||||
Username: tgToken.Username,
|
||||
Contact: req.TelegramContact,
|
||||
}
|
||||
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
if app.storage.telegram == nil {
|
||||
app.storage.telegram = map[string]TelegramUser{}
|
||||
}
|
||||
app.storage.telegram[user.ID] = tgUser
|
||||
err := app.storage.storeTelegramUsers()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
} else {
|
||||
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
|
||||
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||
}
|
||||
}
|
||||
|
||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 {
|
||||
name := app.getAddressOrName(user.ID)
|
||||
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
||||
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct welcome email: %v", req.Username, err)
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
app.err.Printf("%s: Failed to send welcome email: %v", req.Username, err)
|
||||
app.err.Printf("%s: Failed to construct welcome message: %v", req.Username, err)
|
||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||
app.err.Printf("%s: Failed to send welcome message: %v", req.Username, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent welcome email to \"%s\"", req.Username, req.Email)
|
||||
app.info.Printf("%s: Sent welcome message to \"%s\"", req.Username, name)
|
||||
}
|
||||
}
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
@ -525,7 +579,20 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
|
||||
"GetUser": map[string]string{},
|
||||
"SetPolicy": map[string]string{},
|
||||
}
|
||||
var addresses []string
|
||||
sendMail := messagesEnabled
|
||||
var msg *Message
|
||||
var err error
|
||||
if sendMail {
|
||||
if req.Enabled {
|
||||
msg, err = app.email.constructEnabled(req.Reason, app, false)
|
||||
} else {
|
||||
msg, err = app.email.constructDisabled(req.Reason, app, false)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct account enabled/disabled emails: %v", err)
|
||||
sendMail = false
|
||||
}
|
||||
}
|
||||
for _, userID := range req.Users {
|
||||
user, status, err := app.jf.UserByID(userID, false)
|
||||
if status != 200 || err != nil {
|
||||
@ -540,31 +607,13 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
|
||||
app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err)
|
||||
continue
|
||||
}
|
||||
if emailEnabled && req.Notify {
|
||||
addr, ok := app.storage.emails[userID]
|
||||
if addr != nil && ok {
|
||||
addresses = append(addresses, addr.(string))
|
||||
if sendMail && req.Notify {
|
||||
if err := app.sendByID(msg, userID); err != nil {
|
||||
app.err.Printf("Failed to send account enabled/disabled email: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(addresses) != 0 {
|
||||
go func(reason string, addresses []string) {
|
||||
var msg *Email
|
||||
var err error
|
||||
if req.Enabled {
|
||||
msg, err = app.email.constructEnabled(reason, app, false)
|
||||
} else {
|
||||
msg, err = app.email.constructDisabled(reason, app, false)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct account enabled/disabled emails: %v", err)
|
||||
} else if err := app.email.send(msg, addresses...); err != nil {
|
||||
app.err.Printf("Failed to send account enabled/disabled emails: %v", err)
|
||||
} else {
|
||||
app.info.Println("Sent account enabled/disabled emails")
|
||||
}
|
||||
}(req.Reason, addresses)
|
||||
}
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 {
|
||||
gc.JSON(500, errors)
|
||||
@ -584,10 +633,19 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
|
||||
// @tags Users
|
||||
func (app *appContext) DeleteUsers(gc *gin.Context) {
|
||||
var req deleteUserDTO
|
||||
var addresses []string
|
||||
gc.BindJSON(&req)
|
||||
errors := map[string]string{}
|
||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||
sendMail := messagesEnabled
|
||||
var msg *Message
|
||||
var err error
|
||||
if sendMail {
|
||||
msg, err = app.email.constructDeleted(req.Reason, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct account deletion emails: %v", err)
|
||||
sendMail = false
|
||||
}
|
||||
}
|
||||
for _, userID := range req.Users {
|
||||
if ombiEnabled {
|
||||
ombiUser, code, err := app.getOmbiUser(userID)
|
||||
@ -610,25 +668,12 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
||||
errors[userID] += msg
|
||||
}
|
||||
}
|
||||
if emailEnabled && req.Notify {
|
||||
addr, ok := app.storage.emails[userID]
|
||||
if addr != nil && ok {
|
||||
addresses = append(addresses, addr.(string))
|
||||
if sendMail && req.Notify {
|
||||
if err := app.sendByID(msg, userID); err != nil {
|
||||
app.err.Printf("Failed to send account deletion email: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(addresses) != 0 {
|
||||
go func(reason string, addresses []string) {
|
||||
msg, err := app.email.constructDeleted(reason, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct account deletion emails: %v", err)
|
||||
} else if err := app.email.send(msg, addresses...); err != nil {
|
||||
app.err.Printf("Failed to send account deletion emails: %v", err)
|
||||
} else {
|
||||
app.info.Println("Sent account deletion emails")
|
||||
}
|
||||
}(req.Reason, addresses)
|
||||
}
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
if len(errors) == len(req.Users) {
|
||||
respondBool(500, false, gc)
|
||||
@ -685,29 +730,21 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
||||
func (app *appContext) Announce(gc *gin.Context) {
|
||||
var req announcementDTO
|
||||
gc.BindJSON(&req)
|
||||
if !emailEnabled {
|
||||
if !messagesEnabled {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
addresses := []string{}
|
||||
for _, userID := range req.Users {
|
||||
addr, ok := app.storage.emails[userID]
|
||||
if !ok || addr == "" {
|
||||
continue
|
||||
}
|
||||
addresses = append(addresses, addr.(string))
|
||||
}
|
||||
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct announcement emails: %v", err)
|
||||
app.err.Printf("Failed to construct announcement messages: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
} else if err := app.email.send(msg, addresses...); err != nil {
|
||||
app.err.Printf("Failed to send announcement emails: %v", err)
|
||||
} else if err := app.sendByID(msg, req.Users...); err != nil {
|
||||
app.err.Printf("Failed to send announcement messages: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.info.Printf("Sent announcement email to %d users", len(addresses))
|
||||
app.info.Println("Sent announcement messages")
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
@ -1137,7 +1174,10 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
if ok {
|
||||
user.Expiry = expiry.Unix()
|
||||
}
|
||||
|
||||
if tgUser, ok := app.storage.telegram[jfUser.ID]; ok {
|
||||
user.Telegram = tgUser.Username
|
||||
user.NotifyThroughTelegram = tgUser.Contact
|
||||
}
|
||||
resp.UserList[i] = user
|
||||
i++
|
||||
}
|
||||
@ -1345,6 +1385,10 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
el := resp.Sections["email"].Settings["language"]
|
||||
el.Options = emailOptions
|
||||
el.Value = app.config.Section("email").Key("language").MustString("en-us")
|
||||
telegramOptions := app.storage.lang.Email.getOptions()
|
||||
tl := resp.Sections["telegram"].Settings["language"]
|
||||
tl.Options = telegramOptions
|
||||
tl.Value = app.config.Section("telegram").Key("language").MustString("en-us")
|
||||
if updater == "" {
|
||||
delete(resp.Sections, "updates")
|
||||
for i, v := range resp.Order {
|
||||
@ -1373,6 +1417,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
resp.Sections["ui"].Settings["language-admin"] = al
|
||||
resp.Sections["email"].Settings["language"] = el
|
||||
resp.Sections["password_resets"].Settings["language"] = pl
|
||||
resp.Sections["telegram"].Settings["language"] = tl
|
||||
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
@ -1397,6 +1442,9 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
tempConfig.NewSection(section)
|
||||
}
|
||||
for setting, value := range settings.(map[string]interface{}) {
|
||||
if section == "email" && setting == "method" && value == "disabled" {
|
||||
value = ""
|
||||
}
|
||||
if value.(string) != app.config.Section(section).Key(setting).MustString("") {
|
||||
tempConfig.Section(section).Key(setting).SetValue(value.(string))
|
||||
}
|
||||
@ -1438,6 +1486,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
// @Param lang query string false "Language for email titles."
|
||||
// @Success 200 {object} emailListDTO
|
||||
// @Router /config/emails [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||
lang := gc.Query("lang")
|
||||
@ -1466,6 +1515,7 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id} [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||
var req customEmail
|
||||
@ -1525,6 +1575,7 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||
// @Param enable/disable path string true "enable/disable"
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||
id := gc.Param("id")
|
||||
@ -1574,13 +1625,14 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id} [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
lang := app.storage.lang.chosenEmailLang
|
||||
id := gc.Param("id")
|
||||
var content string
|
||||
var err error
|
||||
var msg *Email
|
||||
var msg *Message
|
||||
var variables []string
|
||||
var conditionals []string
|
||||
var values map[string]interface{}
|
||||
@ -1762,6 +1814,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} checkUpdateDTO
|
||||
// @Router /config/update [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) CheckUpdate(gc *gin.Context) {
|
||||
if !app.newUpdate {
|
||||
@ -1776,6 +1829,7 @@ func (app *appContext) CheckUpdate(gc *gin.Context) {
|
||||
// @Success 400 {object} stringResponse
|
||||
// @Success 500 {object} boolResponse
|
||||
// @Router /config/update [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
if !app.update.CanUpdate {
|
||||
@ -1801,6 +1855,7 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /logout [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) Logout(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
@ -1874,8 +1929,162 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
||||
respondBool(400, false, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a new Telegram verification PIN, and the bot username.
|
||||
// @Produce json
|
||||
// @Success 200 {object} telegramPinDTO
|
||||
// @Router /telegram/pin [get]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramGetPin(gc *gin.Context) {
|
||||
gc.JSON(200, telegramPinDTO{
|
||||
Token: app.telegram.NewAuthToken(),
|
||||
Username: app.telegram.username,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Link a Jellyfin & Telegram user together via a verification PIN.
|
||||
// @Produce json
|
||||
// @Param telegramSetDTO body telegramSetDTO true "Token and user's Jellyfin ID."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Router /users/telegram [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
var req telegramSetDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.Token == "" || req.ID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
tokenIndex := -1
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
if v.Token == req.Token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
tgToken := app.telegram.verifiedTokens[tokenIndex]
|
||||
tgUser := TelegramUser{
|
||||
ChatID: tgToken.ChatID,
|
||||
Username: tgToken.Username,
|
||||
Contact: true,
|
||||
}
|
||||
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
if app.storage.telegram == nil {
|
||||
app.storage.telegram = map[string]TelegramUser{}
|
||||
}
|
||||
app.storage.telegram[req.ID] = tgUser
|
||||
err := app.storage.storeTelegramUsers()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
} else {
|
||||
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
|
||||
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Sets whether to notify a user through telegram or not.
|
||||
// @Produce json
|
||||
// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 400 {object} boolResponse
|
||||
// @Success 500 {object} boolResponse
|
||||
// @Router /users/telegram/notify [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramSetNotify(gc *gin.Context) {
|
||||
var req telegramNotifyDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.ID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||
tgUser.Contact = req.Enabled
|
||||
app.storage.telegram[req.ID] = tgUser
|
||||
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Telegram: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
msg := ""
|
||||
if !req.Enabled {
|
||||
msg = "not"
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
|
||||
respondBool(400, false, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Router /telegram/verified/{pin} [get]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
tokenIndex := -1
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
if v.Token == pin {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// if tokenIndex != -1 {
|
||||
// length := len(app.telegram.verifiedTokens)
|
||||
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
|
||||
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
|
||||
// }
|
||||
respondBool(200, tokenIndex != -1, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /invite/{invCode}/telegram/verified/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
tokenIndex := -1
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
if v.Token == pin {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// if tokenIndex != -1 {
|
||||
// length := len(app.telegram.verifiedTokens)
|
||||
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
|
||||
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
|
||||
// }
|
||||
respondBool(200, tokenIndex != -1, gc)
|
||||
}
|
||||
|
||||
// @Summary Restarts the program. No response means success.
|
||||
// @Router /restart [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) restart(gc *gin.Context) {
|
||||
app.info.Println("Restarting...")
|
||||
|
41
config.go
41
config.go
@ -12,6 +12,8 @@ import (
|
||||
)
|
||||
|
||||
var emailEnabled = false
|
||||
var messagesEnabled = false
|
||||
var telegramEnabled = false
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
@ -40,7 +42,7 @@ func (app *appContext) loadConfig() error {
|
||||
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"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
@ -83,12 +85,19 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
|
||||
if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
if !messagesEnabled {
|
||||
emailEnabled = false
|
||||
telegramEnabled = false
|
||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
emailEnabled = false
|
||||
} else {
|
||||
emailEnabled = true
|
||||
}
|
||||
if !emailEnabled && !telegramEnabled {
|
||||
messagesEnabled = false
|
||||
}
|
||||
|
||||
app.MustSetValue("updates", "enabled", "true")
|
||||
releaseChannel := app.config.Section("updates").Key("channel").String()
|
||||
@ -128,8 +137,34 @@ func (app *appContext) loadConfig() error {
|
||||
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
|
||||
app.storage.lang.chosenPWRLang = app.config.Section("password_resets").Key("language").MustString("en-us")
|
||||
app.storage.lang.chosenTelegramLang = app.config.Section("telegram").Key("language").MustString("en-us")
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) migrateEmailConfig() {
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
|
||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to backup config: %v", err)
|
||||
return
|
||||
}
|
||||
for _, setting := range []string{"use_24h", "date_format", "message"} {
|
||||
if val := app.config.Section("email").Key(setting).Value(); val != "" {
|
||||
tempConfig.Section("email").Key(setting).SetValue("")
|
||||
tempConfig.Section("messages").Key(setting).SetValue(val)
|
||||
}
|
||||
}
|
||||
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
|
||||
tempConfig.Section("messages").Key("enabled").SetValue("true")
|
||||
}
|
||||
err = tempConfig.SaveTo(app.configPath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to save config: %v", err)
|
||||
return
|
||||
}
|
||||
app.loadConfig()
|
||||
}
|
||||
|
@ -345,33 +345,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"messages": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Email",
|
||||
"description": "General email settings."
|
||||
"name": "Messages/Notifications",
|
||||
"description": "General settings for emails/messages."
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"name": "Email Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default email language. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use email address from invite form as username on Jellyfin."
|
||||
"value": true,
|
||||
"description": "Enable the sending of emails/messages such as password resets, announcements, etc."
|
||||
},
|
||||
"use_24h": {
|
||||
"name": "Use 24h time",
|
||||
@ -399,6 +386,37 @@
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Message displayed at bottom of emails."
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Email",
|
||||
"description": "General email settings.",
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"name": "Email Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default email language. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use email address from invite form as username on Jellyfin."
|
||||
},
|
||||
"method": {
|
||||
"name": "Email method",
|
||||
@ -443,12 +461,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
"description": "Mailgun API connection settings",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"api_url": {
|
||||
"name": "API URL",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://api.mailgun.net..."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "your api key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings.",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Username for SMTP. Leave blank to user send from address as username."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
["ssl_tls", "SSL/TLS"],
|
||||
["starttls", "STARTTLS"]
|
||||
],
|
||||
"value": "starttls",
|
||||
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "smtp.jellyf.in",
|
||||
"description": "SMTP Server address."
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "number",
|
||||
"value": 465
|
||||
},
|
||||
"password": {
|
||||
"name": "Password",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "password",
|
||||
"value": "smtp password"
|
||||
},
|
||||
"ssl_cert": {
|
||||
"name": "Path to custom SSL certificate",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Use if your SMTP server's SSL Certificate is not trusted by the system."
|
||||
}
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Telegram",
|
||||
"description": "Settings for Telegram signup/notifications"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable signup verification through Telegram 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,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require telegram connection on sign-up."
|
||||
},
|
||||
"token": {
|
||||
"name": "API Token",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Telegram Bot API Token."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default telegram message language. Visit weblate if you'd like to translate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"password_resets": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Password Resets",
|
||||
"description": "Settings for the password reset handler.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@ -580,7 +729,7 @@
|
||||
"meta": {
|
||||
"name": "Notifications",
|
||||
"description": "Notification related settings.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@ -633,91 +782,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
"description": "Mailgun API connection settings",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"api_url": {
|
||||
"name": "API URL",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://api.mailgun.net..."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "your api key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings.",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Username for SMTP. Leave blank to user send from address as username."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
["ssl_tls", "SSL/TLS"],
|
||||
["starttls", "STARTTLS"]
|
||||
],
|
||||
"value": "starttls",
|
||||
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "smtp.jellyf.in",
|
||||
"description": "SMTP Server address."
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "number",
|
||||
"value": 465
|
||||
},
|
||||
"password": {
|
||||
"name": "Password",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "password",
|
||||
"value": "smtp password"
|
||||
},
|
||||
"ssl_cert": {
|
||||
"name": "Path to custom SSL certificate",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Use if your SMTP server's SSL Certificate is not trusted by the system."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ombi": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@ -756,9 +820,9 @@
|
||||
"welcome_email": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Welcome Emails",
|
||||
"description": "Optionally send a welcome email to new users with the Jellyfin URL and their username.",
|
||||
"depends_true": "email|method"
|
||||
"name": "Welcome Message",
|
||||
"description": "Optionally send a welcome message to new users with the Jellyfin URL and their username.",
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@ -865,14 +929,14 @@
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"description": "Send an email when a user's account expires."
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Subject of user expiry emails."
|
||||
@ -882,7 +946,7 @@
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
@ -892,7 +956,7 @@
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
@ -904,7 +968,7 @@
|
||||
"meta": {
|
||||
"name": "Account Disabling/Enabling",
|
||||
"description": "Subject/email files for account disabling/enabling emails.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"subject_disabled": {
|
||||
@ -966,7 +1030,7 @@
|
||||
"meta": {
|
||||
"name": "Account Deletion",
|
||||
"description": "Subject/email files for account deletion emails.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"subject": {
|
||||
@ -1068,6 +1132,14 @@
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info."
|
||||
},
|
||||
"telegram_users": {
|
||||
"name": "Telegram users",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores telegram user IDs and language preferences."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
css/base.css
20
css/base.css
@ -39,6 +39,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chip.btn:hover:not([disabled]):not(.textarea),
|
||||
.chip.btn:focus:not([disabled]):not(.textarea) {
|
||||
filter: brightness(var(--button-filter-brightness,95%));
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: calc(-1 * var(--spacing-4,1rem));
|
||||
}
|
||||
@ -121,6 +126,10 @@ div.card:contains(section.banner.footer) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ac {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -162,6 +171,12 @@ div.card:contains(section.banner.footer) {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
p.sm,
|
||||
span.sm {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.col.sm {
|
||||
margin: .25rem;
|
||||
}
|
||||
@ -459,6 +474,11 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
||||
color: var(--color-urge-200);
|
||||
}
|
||||
|
||||
.link-center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 15rem;
|
||||
min-width: 10rem;
|
||||
|
188
email.go
188
email.go
@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -25,13 +26,13 @@ import (
|
||||
)
|
||||
|
||||
// implements email sending, right now via smtp or mailgun.
|
||||
type emailClient interface {
|
||||
send(fromName, fromAddr string, email *Email, address ...string) error
|
||||
type EmailClient interface {
|
||||
Send(fromName, fromAddr string, message *Message, address ...string) error
|
||||
}
|
||||
|
||||
type dummyClient struct{}
|
||||
type DummyClient struct{}
|
||||
|
||||
func (dc *dummyClient) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
func (dc *DummyClient) Send(fromName, fromAddr string, email *Message, address ...string) error {
|
||||
fmt.Printf("FROM: %s <%s>\nTO: %s\nTEXT: %s\n", fromName, fromAddr, strings.Join(address, ", "), email.Text)
|
||||
return nil
|
||||
}
|
||||
@ -41,7 +42,7 @@ type Mailgun struct {
|
||||
client *mailgun.MailgunImpl
|
||||
}
|
||||
|
||||
func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
func (mg *Mailgun) Send(fromName, fromAddr string, email *Message, address ...string) error {
|
||||
message := mg.client.NewMessage(
|
||||
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
||||
email.Subject,
|
||||
@ -67,7 +68,7 @@ type SMTP struct {
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
func (sm *SMTP) Send(fromName, fromAddr string, email *Message, address ...string) error {
|
||||
server := fmt.Sprintf("%s:%d", sm.server, sm.port)
|
||||
from := fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
||||
var wg sync.WaitGroup
|
||||
@ -93,18 +94,19 @@ func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string)
|
||||
return err
|
||||
}
|
||||
|
||||
// Emailer contains the email sender, email content, and methods to construct message content.
|
||||
// Emailer contains the email sender, translations, and methods to construct messages.
|
||||
type Emailer struct {
|
||||
fromAddr, fromName string
|
||||
lang emailLang
|
||||
sender emailClient
|
||||
sender EmailClient
|
||||
}
|
||||
|
||||
// Email stores content.
|
||||
type Email struct {
|
||||
Subject string `json:"subject"`
|
||||
HTML string `json:"html"`
|
||||
Text string `json:"text"`
|
||||
// Message stores content.
|
||||
type Message struct {
|
||||
Subject string `json:"subject"`
|
||||
HTML string `json:"html"`
|
||||
Text string `json:"text"`
|
||||
Markdown string `json:"markdown"`
|
||||
}
|
||||
|
||||
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) {
|
||||
@ -154,7 +156,7 @@ func NewEmailer(app *appContext) *Emailer {
|
||||
} else if method == "mailgun" {
|
||||
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
|
||||
} else if method == "dummy" {
|
||||
emailer.sender = &dummyClient{}
|
||||
emailer.sender = &DummyClient{}
|
||||
}
|
||||
return emailer
|
||||
}
|
||||
@ -204,7 +206,7 @@ type templ interface {
|
||||
Execute(wr io.Writer, data interface{}) error
|
||||
}
|
||||
|
||||
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) {
|
||||
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text, markdown string, err error) {
|
||||
var tpl templ
|
||||
if substituteStrings == "" {
|
||||
data["jellyfin"] = "Jellyfin"
|
||||
@ -212,14 +214,31 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
||||
data["jellyfin"] = substituteStrings
|
||||
}
|
||||
var keys []string
|
||||
if app.config.Section("email").Key("plaintext").MustBool(false) {
|
||||
keys = []string{"text"}
|
||||
text = ""
|
||||
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
|
||||
telegram := app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
if plaintext {
|
||||
if telegram {
|
||||
keys = []string{"text"}
|
||||
text, markdown = "", ""
|
||||
} else {
|
||||
keys = []string{"text"}
|
||||
text = ""
|
||||
}
|
||||
} else {
|
||||
keys = []string{"html", "text"}
|
||||
if telegram {
|
||||
keys = []string{"html", "text", "markdown"}
|
||||
} else {
|
||||
keys = []string{"html", "text"}
|
||||
}
|
||||
}
|
||||
for _, key := range keys {
|
||||
filesystem, fpath := app.GetPath(section, keyFragment+key)
|
||||
var filesystem fs.FS
|
||||
var fpath string
|
||||
if key == "markdown" {
|
||||
filesystem, fpath = app.GetPath(section, keyFragment+"text")
|
||||
} else {
|
||||
filesystem, fpath = app.GetPath(section, keyFragment+key)
|
||||
}
|
||||
if key == "html" {
|
||||
tpl, err = template.ParseFS(filesystem, fpath)
|
||||
} else {
|
||||
@ -228,15 +247,28 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// For constructTemplate, if "md" is found in data it's used in stead of "text".
|
||||
foundMarkdown := false
|
||||
if key == "markdown" {
|
||||
_, foundMarkdown = data["md"]
|
||||
if foundMarkdown {
|
||||
data["plaintext"], data["md"] = data["md"], data["plaintext"]
|
||||
}
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if foundMarkdown {
|
||||
data["plaintext"], data["md"] = data["md"], data["plaintext"]
|
||||
}
|
||||
if key == "html" {
|
||||
html = tplData.String()
|
||||
} else {
|
||||
} else if key == "text" {
|
||||
text = tplData.String()
|
||||
} else {
|
||||
markdown = tplData.String()
|
||||
}
|
||||
}
|
||||
return
|
||||
@ -257,7 +289,7 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
||||
template[v] = "{" + v + "}"
|
||||
}
|
||||
} else {
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
|
||||
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
|
||||
@ -267,8 +299,8 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
|
||||
}
|
||||
var err error
|
||||
@ -282,7 +314,7 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "email_confirmation", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "email_confirmation", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -290,17 +322,18 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) {
|
||||
email := &Email{Subject: subject}
|
||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Message, error) {
|
||||
email := &Message{Subject: subject}
|
||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||
text := stripMarkdown(md)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
var err error
|
||||
email.HTML, email.Text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
|
||||
"text": template.HTML(html),
|
||||
"plaintext": text,
|
||||
"message": message,
|
||||
"md": md,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -311,7 +344,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
|
||||
func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
|
||||
expiry := invite.ValidTill
|
||||
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
|
||||
template := map[string]interface{}{
|
||||
@ -338,8 +371,8 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
|
||||
}
|
||||
template := emailer.inviteValues(code, invite, app, noSub)
|
||||
@ -353,7 +386,7 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "invite_emails", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "invite_emails", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -377,8 +410,8 @@ func (emailer *Emailer) expiryValues(code string, invite Invite, app *appContext
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: emailer.lang.InviteExpiry.get("title"),
|
||||
}
|
||||
var err error
|
||||
@ -392,7 +425,7 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "notifications", "expiry_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "notifications", "expiry_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -431,8 +464,8 @@ func (emailer *Emailer) createdValues(code, username, address string, invite Inv
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: emailer.lang.UserCreated.get("title"),
|
||||
}
|
||||
template := emailer.createdValues(code, username, address, invite, app, noSub)
|
||||
@ -446,7 +479,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "notifications", "created_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "notifications", "created_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -456,7 +489,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
|
||||
func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
|
||||
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
template := map[string]interface{}{
|
||||
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
|
||||
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
||||
@ -505,8 +538,8 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
|
||||
}
|
||||
template := emailer.resetValues(pwr, app, noSub)
|
||||
@ -520,7 +553,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "password_resets", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "password_resets", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -541,13 +574,13 @@ func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool
|
||||
}
|
||||
} else {
|
||||
template["reason"] = reason
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
|
||||
}
|
||||
var err error
|
||||
@ -561,7 +594,7 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "deletion", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "deletion", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -582,13 +615,13 @@ func (emailer *Emailer) disabledValues(reason string, app *appContext, noSub boo
|
||||
}
|
||||
} else {
|
||||
template["reason"] = reason
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("disable_enable").Key("subject_disabled").MustString(emailer.lang.UserDisabled.get("title")),
|
||||
}
|
||||
var err error
|
||||
@ -602,7 +635,7 @@ func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "disable_enable", "disabled_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "disable_enable", "disabled_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -623,13 +656,13 @@ func (emailer *Emailer) enabledValues(reason string, app *appContext, noSub bool
|
||||
}
|
||||
} else {
|
||||
template["reason"] = reason
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("disable_enable").Key("subject_enabled").MustString(emailer.lang.UserEnabled.get("title")),
|
||||
}
|
||||
var err error
|
||||
@ -643,7 +676,7 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "disable_enable", "enabled_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "disable_enable", "enabled_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -668,7 +701,7 @@ func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *ap
|
||||
} else {
|
||||
template["jellyfinURL"] = app.config.Section("jellyfin").Key("public_server").String()
|
||||
template["username"] = username
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
exp := app.formatDatetime(expiry)
|
||||
if !expiry.IsZero() {
|
||||
if custom {
|
||||
@ -683,8 +716,8 @@ func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *ap
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
|
||||
}
|
||||
var err error
|
||||
@ -708,7 +741,7 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "welcome_email", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "welcome_email", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -723,13 +756,13 @@ func (emailer *Emailer) userExpiredValues(app *appContext, noSub bool) map[strin
|
||||
"message": "",
|
||||
}
|
||||
if !noSub {
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("user_expiry").Key("subject").MustString(emailer.lang.UserExpired.get("title")),
|
||||
}
|
||||
var err error
|
||||
@ -743,7 +776,7 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Emai
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "user_expiry", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "user_expiry", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -752,6 +785,31 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Emai
|
||||
}
|
||||
|
||||
// calls the send method in the underlying emailClient.
|
||||
func (emailer *Emailer) send(email *Email, address ...string) error {
|
||||
return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||
func (emailer *Emailer) send(email *Message, address ...string) error {
|
||||
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||
}
|
||||
|
||||
func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||
for _, id := range ID {
|
||||
var err error
|
||||
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
||||
err = app.telegram.Send(email, tgChat.ChatID)
|
||||
} else if address, ok := app.storage.emails[id]; ok {
|
||||
err = app.email.send(email, address.(string))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) getAddressOrName(jfID string) string {
|
||||
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
|
||||
return "@" + tgChat.Username
|
||||
}
|
||||
if addr, ok := app.storage.emails[jfID]; ok {
|
||||
return addr.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
13
go.mod
13
go.mod
@ -17,29 +17,28 @@ require (
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/mediabrowser v0.3.3
|
||||
github.com/itchyny/timefmt-go v0.1.2
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.7.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
|
26
go.sum
26
go.sum
@ -58,9 +58,6 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@ -96,6 +93,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
@ -112,8 +111,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 h1:nWU6p08f1VgIalT6iZyqXi4o5cZsz4X6qa87nusfcsc=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd h1:0b8AqsWQb6A0jjx80UXLG/uMTXQkGD0IGuXWqsrNz1M=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@ -127,12 +126,14 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
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/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
|
||||
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/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@ -156,8 +157,8 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0 h1:9nAF7LI3k6bfDPbMZQMMl63Q8/vs+dr1FUN8eR1XMhk=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8onwqcRy7joSJI=
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1 h1:XrQQ/ZgqFvINRKy+eBqowLl7k3pQO6OCLpKphliMOFs=
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@ -180,9 +181,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
@ -197,8 +197,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
@ -216,6 +214,8 @@ github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+t
|
||||
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
|
||||
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
|
||||
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
|
@ -6,6 +6,7 @@
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.emailEnabled = {{ .email_enabled }};
|
||||
window.telegramEnabled = {{ .telegram_enabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
@ -179,8 +180,8 @@
|
||||
</div>
|
||||
<div id="modal-customize" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading">{{ .strings.customizeEmails }} <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.customizeEmailsDescription }}</p>
|
||||
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.customizeMessagesDescription }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@ -308,6 +309,24 @@
|
||||
<span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .telegram_enabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-1">{{ .strings.sendPIN }}</p>
|
||||
<h1 class="ac" id="telegram-pin"></h1>
|
||||
<a class="subheading link-center" id="telegram-link" target="_blank">
|
||||
<span class="shield ~info mr-1">
|
||||
<span class="icon">
|
||||
<i class="ri-telegram-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
@<span id="telegram-username">
|
||||
</a>
|
||||
<span class="button ~info !normal full-width center mt-1" id="telegram-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="notification-box"></div>
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
@ -503,6 +522,9 @@
|
||||
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||
<th>{{ .strings.username }}</th>
|
||||
<th>{{ .strings.emailAddress }}</th>
|
||||
{{ if .telegram_enabled }}
|
||||
<th>Telegram</th>
|
||||
{{ end }}
|
||||
<th>{{ .strings.expiry }}</th>
|
||||
<th>{{ .strings.lastActiveTime }}</th>
|
||||
</tr>
|
||||
|
@ -14,6 +14,9 @@
|
||||
window.userExpiryHours = {{ .userExpiryHours }};
|
||||
window.userExpiryMinutes = {{ .userExpiryMinutes }};
|
||||
window.userExpiryMessage = {{ .userExpiryMessage }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramPIN = "{{ .telegramPIN }}";
|
||||
</script>
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ end }}
|
||||
|
@ -19,6 +19,24 @@
|
||||
<p class="content mb-1">{{ .strings.confirmationRequiredMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .telegramEnabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-1">{{ .strings.sendPIN }}</p>
|
||||
<h1 class="ac">{{ .telegramPIN }}</h1>
|
||||
<a class="subheading link-center" href="{{ .telegramURL }}" target="_blank">
|
||||
<span class="shield ~info mr-1">
|
||||
<span class="icon">
|
||||
<i class="ri-telegram-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
@{{ .telegramUsername }}
|
||||
</a>
|
||||
<span class="button ~info !normal full-width center mt-1" id="telegram-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
@ -29,6 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<div id="notification-box"></div>
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral !low">
|
||||
<div class="row baseline">
|
||||
@ -48,7 +67,17 @@
|
||||
|
||||
<label class="label supra" for="create-email">{{ .strings.emailAddress }}</label>
|
||||
<input type="email" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
|
||||
|
||||
{{ if .telegramEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-telegram">{{ .strings.linkTelegram }}</span>
|
||||
<div id="contact-via" class="unfocused">
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
|
||||
</label>
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
|
||||
</label>
|
||||
</div>
|
||||
{{ end }}
|
||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 66 KiB |
@ -13,7 +13,7 @@ const binaryType = "internal"
|
||||
//go:embed data data/html data/web data/web/css data/web/js
|
||||
var loFS embed.FS
|
||||
|
||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset
|
||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram
|
||||
var laFS embed.FS
|
||||
|
||||
var langFS rewriteFS
|
||||
|
17
lang.go
17
lang.go
@ -136,6 +136,23 @@ func (ls *setupLangs) getOptions() [][2]string {
|
||||
return opts
|
||||
}
|
||||
|
||||
type telegramLangs map[string]telegramLang
|
||||
|
||||
type telegramLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
}
|
||||
|
||||
func (ts *telegramLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ts))
|
||||
i := 0
|
||||
for key, lang := range *ts {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type langSection map[string]string
|
||||
type tmpl map[string]string
|
||||
|
||||
|
@ -69,8 +69,8 @@
|
||||
"preview": "Vorschau",
|
||||
"reset": "Zurücksetzen",
|
||||
"edit": "Bearbeiten",
|
||||
"customizeEmails": "E-Mails anpassen",
|
||||
"customizeEmailsDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"customizeMessages": "E-Mails anpassen",
|
||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"announce": "Ankündigen",
|
||||
"subject": "E-Mail-Betreff",
|
||||
"message": "Nachricht",
|
||||
|
@ -69,9 +69,9 @@
|
||||
"preview": "Προεπισκόπηση",
|
||||
"reset": "Επαναφορά",
|
||||
"edit": "Επεξεργασία",
|
||||
"customizeEmails": "Παραμετροποίηση Emails",
|
||||
"customizeMessages": "Παραμετροποίηση Emails",
|
||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
||||
"customizeEmailsDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
"updates": "Ενημερώσεις",
|
||||
"update": "Ενημέρωση",
|
||||
"download": "Λήψη",
|
||||
|
@ -46,7 +46,7 @@
|
||||
"unknown": "Unknown",
|
||||
"label": "Label",
|
||||
"announce": "Announce",
|
||||
"subject": "Email Subject",
|
||||
"subject": "Subject",
|
||||
"message": "Message",
|
||||
"variables": "Variables",
|
||||
"conditionals": "Conditionals",
|
||||
@ -54,14 +54,15 @@
|
||||
"reset": "Reset",
|
||||
"edit": "Edit",
|
||||
"donate": "Donate",
|
||||
"contactThrough": "Contact through:",
|
||||
"extendExpiry": "Extend expiry",
|
||||
"customizeEmails": "Customize Emails",
|
||||
"customizeEmailsDescription": "If you don't want to use jfa-go's email templates, you can create your own using Markdown.",
|
||||
"customizeMessages": "Customize Messages",
|
||||
"customizeMessagesDescription": "If you don't want to use jfa-go's message templates, you can create your own using Markdown.",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
"modifySettings": "Modify Settings",
|
||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
"sendDeleteNotificationEmail": "Send notification email",
|
||||
"sendDeleteNotificationEmail": "Send notification message",
|
||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||
"settingsRestart": "Restart",
|
||||
"settingsRestarting": "Restarting…",
|
||||
@ -92,7 +93,8 @@
|
||||
"inviteExpiresInTime": "Expires in {n}",
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"notifyUserCreation": "On user creation"
|
||||
"notifyUserCreation": "On user creation",
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
@ -104,6 +106,7 @@
|
||||
"setOmbiDefaults": "Stored ombi defaults.",
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
@ -126,7 +129,7 @@
|
||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
|
||||
"errorUserCreated": "Failed to create user {n}.",
|
||||
"errorSendWelcomeEmail": "Failed to send welcome email (check console/logs)",
|
||||
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
|
||||
"errorApplyUpdate": "Failed to apply update, try manually.",
|
||||
"errorCheckUpdate": "Failed to check for update.",
|
||||
"updateAvailable": "A new update is available, check settings.",
|
||||
|
@ -53,8 +53,8 @@
|
||||
"reset": "Reiniciar",
|
||||
"edit": "Editar",
|
||||
"extendExpiry": "Extender el vencimiento",
|
||||
"customizeEmails": "Personalizar emails",
|
||||
"customizeEmailsDescription": "Si no desea utilizar las plantillas de correo electrónico de jfa-go, puede crear las suyas propias con Markdown.",
|
||||
"customizeMessages": "Personalizar emails",
|
||||
"customizeMessagesDescription": "Si no desea utilizar las plantillas de correo electrónico de jfa-go, puede crear las suyas propias con Markdown.",
|
||||
"markdownSupported": "Se admite Markdown.",
|
||||
"modifySettings": "Modificar configuración",
|
||||
"modifySettingsDescription": "Aplique la configuración de un perfil existente u obténgalos directamente de un usuario.",
|
||||
|
@ -68,15 +68,15 @@
|
||||
"settingsRestarting": "Redémarrage…",
|
||||
"settingsRestart": "Redémarrer",
|
||||
"announce": "Annoncer",
|
||||
"subject": "Sujet du courriel",
|
||||
"subject": "Sujet",
|
||||
"message": "Message",
|
||||
"markdownSupported": "Markdown est pris en charge.",
|
||||
"customizeEmailsDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.",
|
||||
"customizeMessagesDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.",
|
||||
"variables": "Variables",
|
||||
"preview": "Aperçu",
|
||||
"reset": "Réinitialiser",
|
||||
"edit": "Éditer",
|
||||
"customizeEmails": "Personnaliser les e-mails",
|
||||
"customizeMessages": "Personnaliser les e-mails",
|
||||
"inviteDuration": "Durée de l'invitation",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
|
@ -69,8 +69,8 @@
|
||||
"preview": "Pratinjau",
|
||||
"reset": "Setel ulang",
|
||||
"edit": "Edit",
|
||||
"customizeEmails": "Sesuaikan Email",
|
||||
"customizeEmailsDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||
"customizeMessages": "Sesuaikan Email",
|
||||
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||
"announce": "Mengumumkan",
|
||||
"subject": "Subjek Email",
|
||||
"message": "Pesan",
|
||||
|
@ -70,11 +70,11 @@
|
||||
"subject": "E-mailonderwerp",
|
||||
"message": "Bericht",
|
||||
"variables": "Variabelen",
|
||||
"customizeEmailsDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
"preview": "Voorbeeld",
|
||||
"reset": "Resetten",
|
||||
"edit": "Bewerken",
|
||||
"customizeEmails": "E-mails aanpassen",
|
||||
"customizeMessages": "E-mails aanpassen",
|
||||
"inviteDuration": "Geldigheidsduur uitnodiging",
|
||||
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
|
||||
"enabled": "Ingeschakeld",
|
||||
|
@ -69,12 +69,12 @@
|
||||
"subject": "Assunto do email",
|
||||
"message": "Mensagem",
|
||||
"markdownSupported": "Suporte a Markdown.",
|
||||
"customizeEmailsDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
"variables": "Variáveis",
|
||||
"preview": "Pre-visualizar",
|
||||
"reset": "Reiniciar",
|
||||
"edit": "Editar",
|
||||
"customizeEmails": "Customizar Emails",
|
||||
"customizeMessages": "Customizar Emails",
|
||||
"disabled": "Desativado",
|
||||
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
|
||||
"inviteDuration": "Duração do Convite",
|
||||
|
@ -37,8 +37,8 @@
|
||||
"preview": "Förhandsvisning",
|
||||
"reset": "Återställ",
|
||||
"edit": "Redigera",
|
||||
"customizeEmails": "Anpassa e-post",
|
||||
"customizeEmailsDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||
"customizeMessages": "Anpassa e-post",
|
||||
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||
"markdownSupported": "Markdown stöds.",
|
||||
"modifySettings": "Ändra inställningar",
|
||||
"modifySettingsDescription": "Tillämpa inställningar från en befintlig profil eller kopiera dem direkt från en användare.",
|
||||
|
@ -14,6 +14,9 @@
|
||||
"copied": "Copied",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"theme": "Theme"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "If this wasn't you, please ignore this email.",
|
||||
"ifItWasNotYou": "If this wasn't you, please ignore this.",
|
||||
"helloUser": "Hi {username},",
|
||||
"reason": "Reason"
|
||||
},
|
||||
@ -55,7 +55,7 @@
|
||||
"linkButton": "Setup your account"
|
||||
},
|
||||
"welcomeEmail": {
|
||||
"name": "Welcome email",
|
||||
"name": "Welcome",
|
||||
"title": "Welcome to Jellyfin",
|
||||
"welcome": "Welcome to Jellyfin!",
|
||||
"youCanLoginWith": "You can login with the details below",
|
||||
|
@ -17,11 +17,15 @@
|
||||
"successContinueButton": "Continue",
|
||||
"confirmationRequired": "Email confirmation required",
|
||||
"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."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "User already exists.",
|
||||
"errorInvalidCode": "Invalid invite code."
|
||||
"errorInvalidCode": "Invalid invite code.",
|
||||
"errorTelegramVerification": "Telegram verification required.",
|
||||
"errorInvalidPIN": "Telegram PIN is invalid.",
|
||||
"telegramVerified": "Telegram account verified."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
22
main.go
22
main.go
@ -93,6 +93,7 @@ type appContext struct {
|
||||
storage Storage
|
||||
validator Validator
|
||||
email *Emailer
|
||||
telegram *TelegramDaemon
|
||||
info, debug, err logger.Logger
|
||||
host string
|
||||
port int
|
||||
@ -198,6 +199,12 @@ func start(asDaemon, firstCall bool) {
|
||||
if err := app.loadConfig(); err != nil {
|
||||
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
|
||||
}
|
||||
|
||||
// Some message settings have been moved from "email" to "messages", this will switch them.
|
||||
if app.config.Section("email").Key("use_24h").Value() != "" {
|
||||
app.migrateEmailConfig()
|
||||
}
|
||||
|
||||
app.version = app.config.Section("jellyfin").Key("version").String()
|
||||
// read from config...
|
||||
debugMode = app.config.Section("ui").Key("debug").MustBool(false)
|
||||
@ -257,6 +264,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.lang.FormPath = "form"
|
||||
app.storage.lang.AdminPath = "admin"
|
||||
app.storage.lang.EmailPath = "email"
|
||||
app.storage.lang.TelegramPath = "telegram"
|
||||
app.storage.lang.PasswordResetPath = "pwreset"
|
||||
externalLang := app.config.Section("files").Key("lang_files").MustString("")
|
||||
var err error
|
||||
@ -325,6 +333,10 @@ func start(asDaemon, firstCall bool) {
|
||||
if err := app.storage.loadUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Users: %v", err)
|
||||
}
|
||||
app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String()
|
||||
if err := app.storage.loadTelegramUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Telegram users: %v", err)
|
||||
}
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
@ -541,6 +553,16 @@ func start(asDaemon, firstCall bool) {
|
||||
if app.config.Section("updates").Key("enabled").MustBool(false) {
|
||||
go app.checkForUpdates()
|
||||
}
|
||||
|
||||
if telegramEnabled {
|
||||
app.telegram, err = newTelegramDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
||||
} else {
|
||||
go app.telegram.run()
|
||||
defer app.telegram.Shutdown()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
address = "0.0.0.0:8056"
|
||||
|
41
models.go
41
models.go
@ -11,10 +11,12 @@ type boolResponse struct {
|
||||
}
|
||||
|
||||
type newUserDTO struct {
|
||||
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
||||
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
||||
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
|
||||
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
|
||||
}
|
||||
|
||||
type newUserResponse struct {
|
||||
@ -120,13 +122,15 @@ type deleteInviteDTO struct {
|
||||
}
|
||||
|
||||
type respUser struct {
|
||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||
Name string `json:"name" example:"jeff"` // Username of user
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||
Name string `json:"name" example:"jeff"` // Username of user
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
Telegram string `json:"telegram"` // Telegram username (if known)
|
||||
NotifyThroughTelegram bool `json:"notify_telegram"`
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
@ -234,3 +238,18 @@ type checkUpdateDTO struct {
|
||||
New bool `json:"new"` // Whether or not there's a new update.
|
||||
Update Update `json:"update"`
|
||||
}
|
||||
|
||||
type telegramPinDTO struct {
|
||||
Token string `json:"token" example:"A1-B2-3C"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type telegramSetDTO struct {
|
||||
Token string `json:"token" example:"A1-B2-3C"`
|
||||
ID string `json:"id"` // Jellyfin ID of user.
|
||||
}
|
||||
|
||||
type telegramNotifyDTO struct {
|
||||
ID string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
29
pwreset.go
29
pwreset.go
@ -69,27 +69,24 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
return
|
||||
}
|
||||
app.storage.loadEmails()
|
||||
var address string
|
||||
uid := user.ID
|
||||
if uid == "" {
|
||||
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
|
||||
return
|
||||
}
|
||||
addr, ok := app.storage.emails[uid]
|
||||
if !ok || addr == nil {
|
||||
app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
|
||||
return
|
||||
}
|
||||
address = addr.(string)
|
||||
msg, err := app.email.constructReset(pwr, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset email to \"%s\"", address)
|
||||
name := app.getAddressOrName(uid)
|
||||
if name != "" {
|
||||
msg, err := app.email.constructReset(pwr, app, false)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset message for %s", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else if err := app.sendByID(msg, uid); err != nil {
|
||||
app.err.Printf("Failed to send password reset message to \"%s\"", name)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
||||
|
@ -118,6 +118,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.POST(p+"/newUser", app.NewUser)
|
||||
router.Use(static.Serve(p+"/invite/", app.webFS))
|
||||
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
||||
if telegramEnabled {
|
||||
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
||||
}
|
||||
}
|
||||
if *SWAGGER {
|
||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
@ -155,6 +158,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
if telegramEnabled {
|
||||
api.GET(p+"/telegram/pin", app.TelegramGetPin)
|
||||
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
|
||||
api.POST(p+"/users/telegram", app.TelegramAddUser)
|
||||
api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
|
||||
|
2
setup.go
2
setup.go
@ -29,7 +29,7 @@ func (app *appContext) ServeSetup(gc *gin.Context) {
|
||||
"success_message": app.config.Section("ui").Key("success_message").String(),
|
||||
},
|
||||
"email": {
|
||||
"message": app.config.Section("email").Key("message").String(),
|
||||
"message": app.config.Section("messages").Key("message").String(),
|
||||
},
|
||||
}
|
||||
msg, err := json.Marshal(messages)
|
||||
|
158
storage.go
158
storage.go
@ -15,18 +15,26 @@ import (
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, displayprefs, ombi_template map[string]interface{}
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, displayprefs, ombi_template map[string]interface{}
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
}
|
||||
|
||||
type TelegramUser struct {
|
||||
ChatID int64
|
||||
Username string
|
||||
Lang string
|
||||
Contact bool // Whether to contact through telegram or not
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
@ -81,23 +89,26 @@ type Invite struct {
|
||||
}
|
||||
|
||||
type Lang struct {
|
||||
AdminPath string
|
||||
chosenAdminLang string
|
||||
Admin adminLangs
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
chosenFormLang string
|
||||
Form formLangs
|
||||
PasswordResetPath string
|
||||
chosenPWRLang string
|
||||
PasswordReset pwrLangs
|
||||
EmailPath string
|
||||
chosenEmailLang string
|
||||
Email emailLangs
|
||||
CommonPath string
|
||||
Common commonLangs
|
||||
SetupPath string
|
||||
Setup setupLangs
|
||||
AdminPath string
|
||||
chosenAdminLang string
|
||||
Admin adminLangs
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
chosenFormLang string
|
||||
Form formLangs
|
||||
PasswordResetPath string
|
||||
chosenPWRLang string
|
||||
PasswordReset pwrLangs
|
||||
EmailPath string
|
||||
chosenEmailLang string
|
||||
Email emailLangs
|
||||
CommonPath string
|
||||
Common commonLangs
|
||||
SetupPath string
|
||||
Setup setupLangs
|
||||
chosenTelegramLang string
|
||||
TelegramPath string
|
||||
Telegram telegramLangs
|
||||
}
|
||||
|
||||
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
@ -118,6 +129,10 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
return
|
||||
}
|
||||
err = st.loadLangEmail(filesystems...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangTelegram(filesystems...)
|
||||
return
|
||||
}
|
||||
|
||||
@ -620,6 +635,83 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
|
||||
st.lang.Telegram = map[string]telegramLang{}
|
||||
var english telegramLang
|
||||
loadedLangs := make([]map[string]bool, len(filesystems))
|
||||
var load loadLangFunc
|
||||
load = func(fsIndex int, fname string) error {
|
||||
filesystem := filesystems[fsIndex]
|
||||
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
||||
lang := telegramLang{}
|
||||
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.TelegramPath, fname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if substituteStrings != "" {
|
||||
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
|
||||
}
|
||||
err = json.Unmarshal(f, &lang)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Telegram[lang.Meta.Fallback]
|
||||
err = nil
|
||||
if !ok {
|
||||
err = load(fsIndex, lang.Meta.Fallback+".json")
|
||||
fallback = st.lang.Telegram[lang.Meta.Fallback]
|
||||
}
|
||||
if err == nil {
|
||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||
}
|
||||
}
|
||||
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
||||
patchLang(&lang.Strings, &english.Strings)
|
||||
}
|
||||
}
|
||||
st.lang.Telegram[index] = lang
|
||||
return nil
|
||||
}
|
||||
engFound := false
|
||||
var err error
|
||||
for i := range filesystems {
|
||||
loadedLangs[i] = map[string]bool{}
|
||||
err = load(i, "en-us.json")
|
||||
if err == nil {
|
||||
engFound = true
|
||||
}
|
||||
loadedLangs[i]["en-us.json"] = true
|
||||
}
|
||||
if !engFound {
|
||||
return err
|
||||
}
|
||||
english = st.lang.Telegram["en-us"]
|
||||
telegramLoaded := false
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.TelegramPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
telegramLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !telegramLoaded {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Invites map[string]Invite
|
||||
|
||||
func (st *Storage) loadInvites() error {
|
||||
@ -665,6 +757,14 @@ func (st *Storage) storeEmails() error {
|
||||
return storeJSON(st.emails_path, st.emails)
|
||||
}
|
||||
|
||||
func (st *Storage) loadTelegramUsers() error {
|
||||
return loadJSON(st.telegram_path, &st.telegram)
|
||||
}
|
||||
|
||||
func (st *Storage) storeTelegramUsers() error {
|
||||
return storeJSON(st.telegram_path, st.telegram)
|
||||
}
|
||||
|
||||
func (st *Storage) loadCustomEmails() error {
|
||||
return loadJSON(st.customEmails_path, &st.customEmails)
|
||||
}
|
||||
|
220
telegram.go
Normal file
220
telegram.go
Normal file
@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
type VerifiedToken struct {
|
||||
Token string
|
||||
ChatID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
type TelegramDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *tg.BotAPI
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens []VerifiedToken
|
||||
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
||||
link string
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
token := app.config.Section("telegram").Key("token").String()
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("token was blank")
|
||||
}
|
||||
bot, err := tg.NewBotAPI(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
td := &TelegramDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
username: bot.Self.UserName,
|
||||
tokens: []string{},
|
||||
verifiedTokens: []VerifiedToken{},
|
||||
languages: map[int64]string{},
|
||||
link: "https://t.me/" + bot.Self.UserName,
|
||||
app: app,
|
||||
}
|
||||
for _, user := range app.storage.telegram {
|
||||
if user.Lang != "" {
|
||||
td.languages[user.ChatID] = user.Lang
|
||||
}
|
||||
}
|
||||
return td, nil
|
||||
}
|
||||
|
||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pin := make([]rune, 8)
|
||||
for i := range pin {
|
||||
if i == 2 || i == 5 {
|
||||
pin[i] = '-'
|
||||
} else {
|
||||
pin[i] = runes[rand.Intn(len(runes))]
|
||||
}
|
||||
}
|
||||
t.tokens = append(t.tokens, string(pin))
|
||||
return string(pin)
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) run() {
|
||||
t.app.info.Println("Starting Telegram bot daemon")
|
||||
u := tg.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
updates, err := t.bot.GetUpdatesChan(u)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
var upd tg.Update
|
||||
select {
|
||||
case upd = <-updates:
|
||||
if upd.Message == nil {
|
||||
continue
|
||||
}
|
||||
sects := strings.Split(upd.Message.Text, " ")
|
||||
if len(sects) == 0 {
|
||||
continue
|
||||
}
|
||||
lang := t.app.storage.lang.chosenTelegramLang
|
||||
storedLang, ok := t.languages[upd.Message.Chat.ID]
|
||||
if !ok {
|
||||
found := false
|
||||
for code := range t.app.storage.lang.Telegram {
|
||||
if code[:2] == upd.Message.From.LanguageCode {
|
||||
lang = code
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
t.languages[upd.Message.Chat.ID] = lang
|
||||
}
|
||||
} else {
|
||||
lang = storedLang
|
||||
}
|
||||
switch msg := sects[0]; msg {
|
||||
case "/start":
|
||||
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
|
||||
err := t.Reply(&upd, content)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
case "/lang":
|
||||
if len(sects) == 1 {
|
||||
list := "/lang <lang>\n"
|
||||
for code := range t.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
||||
}
|
||||
err := t.Reply(&upd, list)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
t.languages[upd.Message.Chat.ID] = sects[1]
|
||||
for jfID, user := range t.app.storage.telegram {
|
||||
if user.ChatID == upd.Message.Chat.ID {
|
||||
user.Lang = sects[1]
|
||||
t.app.storage.telegram[jfID] = user
|
||||
err := t.app.storage.storeTelegramUsers()
|
||||
if err != nil {
|
||||
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
default:
|
||||
tokenIndex := -1
|
||||
for i, token := range t.tokens {
|
||||
if upd.Message.Text == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
|
||||
Token: upd.Message.Text,
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
Username: upd.Message.Chat.UserName,
|
||||
})
|
||||
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
|
||||
t.tokens = t.tokens[:len(t.tokens)-1]
|
||||
}
|
||||
|
||||
case <-t.ShutdownChannel:
|
||||
t.bot.StopReceivingUpdates()
|
||||
t.ShutdownChannel <- "Down"
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) Reply(upd *tg.Update, content string) error {
|
||||
msg := tg.NewMessage((*upd).Message.Chat.ID, content)
|
||||
_, err := t.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
|
||||
msg := tg.NewMessage((*upd).Message.Chat.ID, content)
|
||||
msg.ReplyToMessageID = (*upd).Message.MessageID
|
||||
_, err := t.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given.
|
||||
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
||||
for _, id := range ID {
|
||||
var msg tg.MessageConfig
|
||||
if message.Markdown == "" {
|
||||
msg = tg.NewMessage(id, message.Text)
|
||||
} else {
|
||||
msg = tg.NewMessage(id, strings.ReplaceAll(message.Markdown, ".", "\\."))
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
}
|
||||
_, err := t.bot.Send(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) Shutdown() {
|
||||
t.Stopped = true
|
||||
t.ShutdownChannel <- "Down"
|
||||
<-t.ShutdownChannel
|
||||
close(t.ShutdownChannel)
|
||||
}
|
@ -62,6 +62,10 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry"));
|
||||
|
||||
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
|
||||
|
||||
if (window.telegramEnabled) {
|
||||
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
|
||||
}
|
||||
})();
|
||||
|
||||
var inviteCreator = new createInvite();
|
||||
|
66
ts/form.ts
66
ts/form.ts
@ -1,15 +1,19 @@
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, toDateString } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
validationStrings: pwValStrings;
|
||||
invalidPassword: string;
|
||||
modal: Modal;
|
||||
successModal: Modal;
|
||||
telegramModal: Modal;
|
||||
confirmationModal: Modal
|
||||
code: string;
|
||||
messages: { [key: string]: string };
|
||||
confirmation: boolean;
|
||||
confirmationModal: Modal
|
||||
telegramRequired: boolean;
|
||||
telegramPIN: string;
|
||||
userExpiryEnabled: boolean;
|
||||
userExpiryMonths: number;
|
||||
userExpiryDays: number;
|
||||
@ -34,7 +38,52 @@ interface pwValStrings {
|
||||
|
||||
loadLangSelector("form");
|
||||
|
||||
window.modal = new Modal(document.getElementById("modal-success"), true);
|
||||
window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement);
|
||||
|
||||
window.animationEvent = whichAnimationEvent();
|
||||
|
||||
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
||||
|
||||
var telegramVerified = false;
|
||||
if (window.telegramEnabled) {
|
||||
window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired);
|
||||
const telegramButton = document.getElementById("link-telegram") as HTMLSpanElement;
|
||||
telegramButton.onclick = () => {
|
||||
const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
|
||||
toggleLoader(waiting);
|
||||
window.telegramModal.show();
|
||||
let modalClosed = false;
|
||||
window.telegramModal.onclose = () => {
|
||||
modalClosed = true;
|
||||
toggleLoader(waiting);
|
||||
}
|
||||
const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 401) {
|
||||
window.telegramModal.close();
|
||||
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
|
||||
return;
|
||||
} else if (req.status == 200) {
|
||||
if (req.response["success"] as boolean) {
|
||||
telegramVerified = true;
|
||||
waiting.classList.add("~positive");
|
||||
waiting.classList.remove("~info");
|
||||
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
|
||||
setTimeout(window.telegramModal.close, 2000);
|
||||
telegramButton.classList.add("unfocused");
|
||||
document.getElementById("contact-via").classList.remove("unfocused");
|
||||
const radio = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
||||
radio.checked = true;
|
||||
} else if (!modalClosed) {
|
||||
setTimeout(checkVerified, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
checkVerified();
|
||||
};
|
||||
}
|
||||
|
||||
if (window.confirmation) {
|
||||
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
|
||||
}
|
||||
@ -110,6 +159,8 @@ interface sendDTO {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
telegram_pin?: string;
|
||||
telegram_contact?: boolean;
|
||||
}
|
||||
|
||||
const create = (event: SubmitEvent) => {
|
||||
@ -121,6 +172,13 @@ const create = (event: SubmitEvent) => {
|
||||
email: emailField.value,
|
||||
password: passwordField.value
|
||||
};
|
||||
if (telegramVerified) {
|
||||
send.telegram_pin = window.telegramPIN;
|
||||
const radio = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
||||
if (radio.checked) {
|
||||
send.telegram_contact = true;
|
||||
}
|
||||
}
|
||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
let vals = req.response as respDTO;
|
||||
@ -130,7 +188,7 @@ const create = (event: SubmitEvent) => {
|
||||
if (!vals[type]) { valid = false; }
|
||||
}
|
||||
if (req.status == 200 && valid) {
|
||||
window.modal.show();
|
||||
window.successModal.show();
|
||||
} else {
|
||||
submitSpan.classList.add("~critical");
|
||||
submitSpan.classList.remove("~urge");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { _get, _post, _delete, toggleLoader, toDateString } from "../modules/common.js";
|
||||
import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString } from "../modules/common.js";
|
||||
import { templateEmail } from "../modules/settings.js";
|
||||
import { Marked } from "@ts-stack/markdown";
|
||||
import { stripMarkdown } from "../modules/stripmd.js";
|
||||
@ -11,6 +11,13 @@ interface User {
|
||||
admin: boolean;
|
||||
disabled: boolean;
|
||||
expiry: number;
|
||||
telegram: string;
|
||||
notify_telegram: boolean;
|
||||
}
|
||||
|
||||
interface getPinResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
class user implements User {
|
||||
@ -22,6 +29,9 @@ class user implements User {
|
||||
private _email: HTMLInputElement;
|
||||
private _emailAddress: string;
|
||||
private _emailEditButton: HTMLElement;
|
||||
private _telegram: HTMLTableDataCellElement;
|
||||
private _telegramUsername: string;
|
||||
private _notifyTelegram: boolean;
|
||||
private _expiry: HTMLTableDataCellElement;
|
||||
private _expiryUnix: number;
|
||||
private _lastActive: HTMLTableDataCellElement;
|
||||
@ -72,6 +82,89 @@ class user implements User {
|
||||
}
|
||||
}
|
||||
|
||||
get telegram(): string { return this._telegramUsername; }
|
||||
set telegram(u: string) {
|
||||
if (!window.telegramEnabled) return;
|
||||
this._telegramUsername = u;
|
||||
if (u == "") {
|
||||
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
|
||||
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
|
||||
} else {
|
||||
this._telegram.innerHTML = `
|
||||
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||
<div class="dropdown manual">
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral !low">
|
||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||
<label class="switch pb-1 mt-half">
|
||||
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||
<span>Email</span>
|
||||
</label>
|
||||
<label class="switch pb-1">
|
||||
<input type="radio" name="accounts-contact-${this.id}">
|
||||
<span>Telegram</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
||||
const button = this._telegram.querySelector("i");
|
||||
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
for (let i = 0; i < radios.length; i++) {
|
||||
radios[i].onclick = this._setTelegramNotify;
|
||||
}
|
||||
|
||||
button.onclick = () => {
|
||||
dropdown.classList.add("selected");
|
||||
document.addEventListener("click", outerClickListener);
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
|
||||
dropdown.classList.remove("selected");
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get notify_telegram(): boolean { return this._notifyTelegram; }
|
||||
set notify_telegram(s: boolean) {
|
||||
if (!window.telegramEnabled || !this._telegramUsername) return;
|
||||
this._notifyTelegram = s;
|
||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
radios[0].checked = !s;
|
||||
radios[1].checked = s;
|
||||
}
|
||||
|
||||
private _setTelegramNotify = () => {
|
||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
let send = {
|
||||
id: this.id,
|
||||
enabled: radios[1].checked
|
||||
};
|
||||
_post("/users/telegram/notify", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings"));
|
||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, false, (req: XMLHttpRequest) => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.connectionError();
|
||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
||||
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get expiry(): number { return this._expiryUnix; }
|
||||
set expiry(unix: number) {
|
||||
this._expiryUnix = unix;
|
||||
@ -97,13 +190,21 @@ class user implements User {
|
||||
|
||||
constructor(user: User) {
|
||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||
this._row.innerHTML = `
|
||||
let innerHTML = `
|
||||
<td><input type="checkbox" value=""></td>
|
||||
<td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
|
||||
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td>
|
||||
<td class="accounts-expiry"></td>
|
||||
<td class="accounts-last-active"></td>
|
||||
`;
|
||||
if (window.telegramEnabled) {
|
||||
innerHTML += `
|
||||
<td class="accounts-telegram"></td>
|
||||
`;
|
||||
}
|
||||
innerHTML += `
|
||||
<td class="accounts-expiry"></td>
|
||||
<td class="accounts-last-active"></td>
|
||||
`;
|
||||
this._row.innerHTML = innerHTML;
|
||||
const emailEditor = `<input type="email" class="input ~neutral !normal stealth-input">`;
|
||||
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
||||
@ -111,6 +212,7 @@ class user implements User {
|
||||
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
|
||||
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
|
||||
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
||||
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
|
||||
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||
@ -169,14 +271,60 @@ class user implements User {
|
||||
});
|
||||
}
|
||||
|
||||
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4 && req.status == 200) {
|
||||
const pin = document.getElementById("telegram-pin");
|
||||
const link = document.getElementById("telegram-link") as HTMLAnchorElement;
|
||||
const username = document.getElementById("telegram-username") as HTMLSpanElement;
|
||||
const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
|
||||
let resp = req.response as getPinResponse;
|
||||
pin.textContent = resp.token;
|
||||
link.href = "https://t.me/" + resp.username;
|
||||
username.textContent = resp.username;
|
||||
addLoader(waiting);
|
||||
let modalClosed = false;
|
||||
window.modals.telegram.onclose = () => {
|
||||
modalClosed = true;
|
||||
removeLoader(waiting);
|
||||
}
|
||||
let send = {
|
||||
token: resp.token,
|
||||
id: this.id
|
||||
};
|
||||
const checkVerified = () => _post("/users/telegram", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 && req.response["success"] as boolean) {
|
||||
removeLoader(waiting);
|
||||
waiting.classList.add("~positive");
|
||||
waiting.classList.remove("~info");
|
||||
window.notifications.customSuccess("telegramVerified", window.lang.notif("telegramVerified"));
|
||||
setTimeout(() => {
|
||||
window.modals.telegram.close();
|
||||
waiting.classList.add("~info");
|
||||
waiting.classList.remove("~positive");
|
||||
}, 2000);
|
||||
document.dispatchEvent(new CustomEvent("accounts-reload"));
|
||||
} else if (!modalClosed) {
|
||||
setTimeout(checkVerified, 1500);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
window.modals.telegram.show();
|
||||
checkVerified();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
update = (user: User) => {
|
||||
this.id = user.id;
|
||||
this.name = user.name;
|
||||
this.email = user.email || "";
|
||||
this.telegram = user.telegram;
|
||||
this.last_active = user.last_active;
|
||||
this.admin = user.admin;
|
||||
this.disabled = user.disabled;
|
||||
this.expiry = user.expiry;
|
||||
this.notify_telegram = user.notify_telegram;
|
||||
}
|
||||
|
||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||
@ -188,9 +336,6 @@ class user implements User {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export class accountsList {
|
||||
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
|
||||
|
||||
@ -334,7 +479,7 @@ export class accountsList {
|
||||
this._selectAll.checked = false;
|
||||
this._modifySettings.classList.add("unfocused");
|
||||
this._deleteUser.classList.add("unfocused");
|
||||
if (window.emailEnabled) {
|
||||
if (window.emailEnabled || window.telegramEnabled) {
|
||||
this._announceButton.classList.add("unfocused");
|
||||
}
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
@ -356,7 +501,7 @@ export class accountsList {
|
||||
this._modifySettings.classList.remove("unfocused");
|
||||
this._deleteUser.classList.remove("unfocused");
|
||||
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
|
||||
if (window.emailEnabled) {
|
||||
if (window.emailEnabled || window.telegramEnabled) {
|
||||
this._announceButton.classList.remove("unfocused");
|
||||
}
|
||||
let anyNonExpiries = list.length == 0 ? true : false;
|
||||
@ -701,6 +846,7 @@ export class accountsList {
|
||||
this._selectAll.onchange = () => {
|
||||
this.selectAll = this._selectAll.checked;
|
||||
};
|
||||
document.addEventListener("accounts-reload", this.reload);
|
||||
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
|
||||
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
|
||||
this._addUserButton.onclick = window.modals.addUser.toggle;
|
||||
|
@ -179,3 +179,22 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) {
|
||||
el.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
export function addLoader(el: HTMLElement, small: boolean = true) {
|
||||
if (!el.classList.contains("loader")) {
|
||||
el.classList.add("loader");
|
||||
if (small) { el.classList.add("loader-sm"); }
|
||||
const dot = document.createElement("span") as HTMLSpanElement;
|
||||
dot.classList.add("dot")
|
||||
el.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeLoader(el: HTMLElement, small: boolean = true) {
|
||||
if (el.classList.contains("loader")) {
|
||||
el.classList.remove("loader");
|
||||
el.classList.remove("loader-sm");
|
||||
const dot = el.querySelector("span.dot");
|
||||
if (dot) { dot.remove(); }
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,13 @@ declare var window: Window;
|
||||
export class Modal implements Modal {
|
||||
modal: HTMLElement;
|
||||
closeButton: HTMLSpanElement;
|
||||
openEvent: CustomEvent;
|
||||
closeEvent: CustomEvent;
|
||||
constructor(modal: HTMLElement, important: boolean = false) {
|
||||
this.modal = modal;
|
||||
const closeButton = this.modal.querySelector('span.modal-close')
|
||||
this.openEvent = new CustomEvent("modal-open-" + modal.id);
|
||||
this.closeEvent = new CustomEvent("modal-close-" + modal.id);
|
||||
const closeButton = this.modal.querySelector('span.modal-close');
|
||||
if (closeButton !== null) {
|
||||
this.closeButton = closeButton as HTMLSpanElement;
|
||||
this.closeButton.onclick = this.close;
|
||||
@ -22,15 +26,25 @@ export class Modal implements Modal {
|
||||
}
|
||||
this.modal.classList.add('modal-hiding');
|
||||
const modal = this.modal;
|
||||
const listenerFunc = function () {
|
||||
const listenerFunc = () => {
|
||||
modal.classList.remove('modal-shown');
|
||||
modal.classList.remove('modal-hiding');
|
||||
modal.removeEventListener(window.animationEvent, listenerFunc);
|
||||
document.dispatchEvent(this.closeEvent);
|
||||
};
|
||||
this.modal.addEventListener(window.animationEvent, listenerFunc, false);
|
||||
}
|
||||
|
||||
set onopen(f: () => void) {
|
||||
document.addEventListener("modal-open-"+this.modal.id, f);
|
||||
}
|
||||
set onclose(f: () => void) {
|
||||
document.addEventListener("modal-close-"+this.modal.id, f);
|
||||
}
|
||||
|
||||
show = () => {
|
||||
this.modal.classList.add('modal-shown');
|
||||
document.dispatchEvent(this.openEvent);
|
||||
}
|
||||
toggle = () => {
|
||||
if (this.modal.classList.contains('modal-shown')) {
|
||||
|
@ -560,10 +560,18 @@ export class settingsList {
|
||||
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
||||
if (Boolean(event.detail) !== state) {
|
||||
button.classList.add("unfocused");
|
||||
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
|
||||
} else {
|
||||
button.classList.remove("unfocused");
|
||||
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: true }));
|
||||
}
|
||||
});
|
||||
document.addEventListener(`settings-${dependant[0]}`, (event: settingsBoolEvent) => {
|
||||
if (Boolean(event.detail) !== state) {
|
||||
button.classList.add("unfocused");
|
||||
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (s.meta.advanced) {
|
||||
document.addEventListener("settings-advancedState", (event: settingsBoolEvent) => {
|
||||
@ -669,7 +677,7 @@ export class settingsList {
|
||||
if (name in this._sections) {
|
||||
this._sections[name].update(settings.sections[name]);
|
||||
} else {
|
||||
if (name == "email") {
|
||||
if (name == "messages") {
|
||||
const editButton = document.createElement("div");
|
||||
editButton.classList.add("tooltip", "left");
|
||||
editButton.innerHTML = `
|
||||
@ -677,7 +685,7 @@ export class settingsList {
|
||||
<i class="icon ri-edit-line"></i>
|
||||
</span>
|
||||
<span class="content sm">
|
||||
${window.lang.get("strings", "customizeEmails")}
|
||||
${window.lang.get("strings", "customizeMessages")}
|
||||
</span>
|
||||
`;
|
||||
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;
|
||||
|
@ -4,6 +4,8 @@ declare interface Modal {
|
||||
show: () => void;
|
||||
close: (event?: Event) => void;
|
||||
toggle: () => void;
|
||||
onopen: (f: () => void) => void;
|
||||
onclose: (f: () => void) => void;
|
||||
}
|
||||
|
||||
interface ArrayConstructor {
|
||||
@ -18,6 +20,7 @@ declare interface Window {
|
||||
jfUsers: Array<Object>;
|
||||
notificationsEnabled: boolean;
|
||||
emailEnabled: boolean;
|
||||
telegramEnabled: boolean;
|
||||
ombiEnabled: boolean;
|
||||
usernameEnabled: boolean;
|
||||
token: string;
|
||||
@ -97,6 +100,7 @@ declare interface Modals {
|
||||
customizeEmails: Modal;
|
||||
extendExpiry: Modal;
|
||||
updateInfo: Modal;
|
||||
telegram: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
@ -71,9 +71,9 @@ func (app *appContext) checkUsers() {
|
||||
mode = "delete"
|
||||
termPlural = "Deleting"
|
||||
}
|
||||
email := false
|
||||
if emailEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||
email = true
|
||||
contact := false
|
||||
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||
contact = true
|
||||
}
|
||||
// Use a map to speed up checking for deleted users later
|
||||
userExists := map[string]bool{}
|
||||
@ -114,18 +114,18 @@ func (app *appContext) checkUsers() {
|
||||
}
|
||||
delete(app.storage.users, id)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
if email {
|
||||
address, ok := app.storage.emails[id]
|
||||
if contact {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := app.getAddressOrName(user.ID)
|
||||
msg, err := app.email.constructUserExpired(app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct expiry email for \"%s\": %s", user.Name, err)
|
||||
} else if err := app.email.send(msg, address.(string)); err != nil {
|
||||
app.err.Printf("Failed to send expiry email to \"%s\": %s", user.Name, err)
|
||||
app.err.Printf("Failed to construct expiry message for \"%s\": %s", user.Name, err)
|
||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to \"%s\"", address.(string))
|
||||
app.info.Printf("Sent expiry notification to \"%s\"", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
views.go
41
views.go
@ -116,20 +116,21 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
}
|
||||
license = string(l)
|
||||
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"strings": app.storage.lang.Admin[lang].Strings,
|
||||
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
|
||||
"language": app.storage.lang.Admin[lang].JSON,
|
||||
"langName": lang,
|
||||
"license": license,
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"telegram_enabled": telegramEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"strings": app.storage.lang.Admin[lang].Strings,
|
||||
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
|
||||
"language": app.storage.lang.Admin[lang].JSON,
|
||||
"langName": lang,
|
||||
"license": license,
|
||||
})
|
||||
}
|
||||
|
||||
@ -259,7 +260,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
if strings.Contains(email, "Failed") {
|
||||
email = ""
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{
|
||||
data := gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
@ -282,7 +283,15 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"userExpiryMinutes": inv.UserMinutes,
|
||||
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
|
||||
"langName": lang,
|
||||
})
|
||||
"telegramEnabled": telegramEnabled,
|
||||
}
|
||||
if data["telegramEnabled"].(bool) {
|
||||
data["telegramPIN"] = app.telegram.NewAuthToken()
|
||||
data["telegramUsername"] = app.telegram.username
|
||||
data["telegramURL"] = app.telegram.link
|
||||
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
||||
}
|
||||
|
||||
func (app *appContext) NoRouteHandler(gc *gin.Context) {
|
||||
|
Loading…
Reference in New Issue
Block a user