Compare commits

...

39 Commits

Author SHA1 Message Date
Harvey Tindall 4c3e310634
use go install for swag in goreleaser 2022-03-22 15:16:04 +00:00
Harvey Tindall 8b52330304
remove unused nightwind dep 2022-03-22 15:09:38 +00:00
Harvey Tindall 200dc1c91a
lowercase lang 2022-03-22 15:03:53 +00:00
3ole 531d4aaefc Translated using Weblate (German)
Currently translated at 100.0% (111 of 111 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/de/
2022-03-22 16:01:21 +01:00
3ole f8d98fb66f Translated using Weblate (German)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/de/
2022-03-22 16:01:21 +01:00
3ole 5b2472853b translation from Weblate (German)
Currently translated at 100.0% (179 of 179 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/de/
2022-03-22 16:01:21 +01:00
mossh 2dc234a94d Translated using Weblate (Arabic)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/password-reset-links/ar/
2022-03-22 16:01:21 +01:00
mossh fdb43b2c53 translation from Weblate (Arabic)
Currently translated at 100.0% (40 of 40 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/ar/
2022-03-22 16:01:21 +01:00
mossh 2161d1aa2f Translated using Weblate (Arabic)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/ar/
2022-03-22 16:01:21 +01:00
mossh 08c8534d39 Added translation using Weblate (Arabic) 2022-03-22 16:01:21 +01:00
mossh 9e7914e0b6 add translation from Weblate (Arabic) 2022-03-22 16:01:21 +01:00
mossh daebfec0a2 Added translation using Weblate (Arabic) 2022-03-22 16:01:21 +01:00
3ole e81411cb9b Translated using Weblate (German)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/de/
2022-03-22 16:01:21 +01:00
3ole 6cdae16752 Translated using Weblate (German)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/password-reset-links/de/
2022-03-22 16:01:21 +01:00
3ole 65a0c6cb23 Translated using Weblate (German)
Currently translated at 100.0% (111 of 111 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/de/
2022-03-22 16:01:21 +01:00
3ole 36d0550bf9 Translated using Weblate (German)
Currently translated at 100.0% (23 of 23 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/de/
2022-03-22 16:01:20 +01:00
3ole 92aee997da Translated using Weblate (German)
Currently translated at 100.0% (51 of 51 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/emails/de/
2022-03-22 16:01:20 +01:00
3ole 63e6f3a3fa translation from Weblate (German)
Currently translated at 100.0% (40 of 40 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/de/
2022-03-22 16:01:20 +01:00
Tim bfafbad9dc translation from Weblate (German)
Currently translated at 100.0% (40 of 40 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/de/
2022-03-22 16:01:20 +01:00
Sundune 4435fead5a translation from Weblate (German)
Currently translated at 100.0% (179 of 179 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/de/
2022-03-22 16:01:20 +01:00
3ole e73715f30e translation from Weblate (German)
Currently translated at 100.0% (179 of 179 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/de/
2022-03-22 16:01:20 +01:00
Herodev f31e9c0d81 translation from Weblate (German)
Currently translated at 95.0% (38 of 40 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/de/
2022-03-22 16:01:20 +01:00
DornJ 2d1a4737db Translated using Weblate (Slovenian)
Currently translated at 100.0% (23 of 23 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/sl/
2022-03-22 16:01:20 +01:00
DornJ 91f89793da Translated using Weblate (Slovenian)
Currently translated at 0.9% (1 of 111 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/sl/
2022-03-22 16:01:20 +01:00
DornJ d201644a5b Translated using Weblate (Slovenian)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/password-reset-links/sl/
2022-03-22 16:01:20 +01:00
DornJ 982dd001ef Translated using Weblate (Slovenian)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/sl/
2022-03-22 16:01:20 +01:00
DornJ 80bd4d134e translation from Weblate (Slovenian)
Currently translated at 100.0% (40 of 40 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/sl/
2022-03-22 16:01:20 +01:00
DornJ 92fda348cd Added translation using Weblate (Slovenian) 2022-03-22 16:01:20 +01:00
DornJ ef3fdb7555 Added translation using Weblate (Slovenian) 2022-03-22 16:01:20 +01:00
DornJ 9eb30ffab3 Added translation using Weblate (Slovenian) 2022-03-22 16:01:20 +01:00
DornJ 0bf6c25a14 Added translation using Weblate (Slovenian) 2022-03-22 16:01:20 +01:00
DornJ 5bb7a7d30c add translation from Weblate (Slovenian) 2022-03-22 16:01:20 +01:00
Richard de Boer 85c9319dfd Translated using Weblate (Dutch)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/nl/
2022-03-22 16:01:20 +01:00
Richard de Boer 3fea051691 Translated using Weblate (Dutch)
Currently translated at 100.0% (111 of 111 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/nl/
2022-03-22 16:01:20 +01:00
Richard de Boer 7826fdffeb Translated using Weblate (Dutch)
Currently translated at 100.0% (23 of 23 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/nl/
2022-03-22 16:01:20 +01:00
Richard de Boer 1105605370 translation from Weblate (Dutch)
Currently translated at 100.0% (40 of 40 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/nl/
2022-03-22 16:01:20 +01:00
Richard de Boer 3de3dd426b translation from Weblate (Dutch)
Currently translated at 100.0% (179 of 179 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/nl/
2022-03-22 16:01:20 +01:00
Richard de Boer 0c7187d53f translation from Weblate (English)
Currently translated at 100.0% (179 of 179 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/en/
2022-03-22 16:01:20 +01:00
Harvey Tindall 12db53d1eb
reorganise api, add "Connection refused" error in setup
fixes #201
2022-03-22 14:58:39 +00:00
35 changed files with 2996 additions and 2525 deletions

View File

@ -39,7 +39,7 @@ before:
- rm data/bundle.css
- npx tailwindcss -i data/web/css/bundle.css -o data/web/css/bundle.css
- mv data/crash.html data/html/
- go get -u github.com/swaggo/swag/cmd/swag
- go install github.com/swaggo/swag/cmd/swag@latest
- swag init -g main.go
- mv data/web/css/bundle.css data/web/css/{{.Env.JFA_GO_CSS_VERSION}}bundle.css
builds:

View File

@ -111,7 +111,7 @@ typescript:
$(COPYTS)
swagger:
$(GOBINARY) install github.com/swaggo/swag/cmd/swag
$(GOBINARY) install github.com/swaggo/swag/cmd/swag@latest
swag init -g main.go
compile:

432
api-invites.go Normal file
View File

@ -0,0 +1,432 @@
package main
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/itchyny/timefmt-go"
"github.com/lithammer/shortuuid/v3"
)
func (app *appContext) checkInvites() {
currentTime := time.Now()
app.storage.loadInvites()
changed := false
for code, data := range app.storage.invites {
expiry := data.ValidTill
if !currentTime.After(expiry) {
continue
}
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := data.Notify
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
app.debug.Printf("%s: Expiry notification", code)
var wait sync.WaitGroup
for address, settings := range notify {
if !settings["notify-expiry"] {
continue
}
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(code, data, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
} else {
// Check whether notify "address" is an email address of Jellyfin ID
if strings.Contains(addr, "@") {
err = app.email.send(msg, addr)
} else {
err = app.sendByID(msg, addr)
}
if err != nil {
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
} else {
app.info.Printf("Sent expiry notification to %s", addr)
}
}
}(address)
}
wait.Wait()
}
changed = true
delete(app.storage.invites, code)
}
if changed {
app.storage.storeInvites()
}
}
func (app *appContext) checkInvite(code string, used bool, username string) bool {
currentTime := time.Now()
app.storage.loadInvites()
changed := false
inv, match := app.storage.invites[code]
if !match {
return false
}
expiry := inv.ValidTill
if currentTime.After(expiry) {
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := inv.Notify
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
app.debug.Printf("%s: Expiry notification", code)
var wait sync.WaitGroup
for address, settings := range notify {
if !settings["notify-expiry"] {
continue
}
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(code, inv, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
} else {
// Check whether notify "address" is an email address of Jellyfin ID
if strings.Contains(addr, "@") {
err = app.email.send(msg, addr)
} else {
err = app.sendByID(msg, addr)
}
if err != nil {
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
} else {
app.info.Printf("Sent expiry notification to %s", addr)
}
}
}(address)
}
wait.Wait()
}
changed = true
match = false
delete(app.storage.invites, code)
} else if used {
changed = true
del := false
newInv := inv
if newInv.RemainingUses == 1 {
del = true
delete(app.storage.invites, code)
} else if newInv.RemainingUses != 0 {
// 0 means infinite i guess?
newInv.RemainingUses--
}
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
if !del {
app.storage.invites[code] = newInv
}
}
if changed {
app.storage.storeInvites()
}
return match
}
// @Summary Create a new invite.
// @Produce json
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
// @Success 200 {object} boolResponse
// @Router /invites [post]
// @Security Bearer
// @tags Invites
func (app *appContext) GenerateInvite(gc *gin.Context) {
var req generateInviteDTO
app.debug.Println("Generating new invite")
app.storage.loadInvites()
gc.BindJSON(&req)
currentTime := time.Now()
validTill := currentTime.AddDate(0, req.Months, req.Days)
validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
// make sure code doesn't begin with number
inviteCode := shortuuid.New()
_, err := strconv.Atoi(string(inviteCode[0]))
for err == nil {
inviteCode = shortuuid.New()
_, err = strconv.Atoi(string(inviteCode[0]))
}
var invite Invite
if req.Label != "" {
invite.Label = req.Label
}
invite.Created = currentTime
if req.MultipleUses {
if req.NoLimit {
invite.NoLimit = true
} else {
invite.RemainingUses = req.RemainingUses
}
} else {
invite.RemainingUses = 1
}
invite.UserExpiry = req.UserExpiry
if invite.UserExpiry {
invite.UserMonths = req.UserMonths
invite.UserDays = req.UserDays
invite.UserHours = req.UserHours
invite.UserMinutes = req.UserMinutes
}
invite.ValidTill = validTill
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
addressValid := false
discord := ""
app.debug.Printf("%s: Sending invite message", inviteCode)
if discordEnabled && !strings.Contains(req.SendTo, "@") {
users := app.discord.GetUsers(req.SendTo)
if len(users) == 0 {
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
} else if len(users) > 1 {
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
} else {
invite.SendTo = req.SendTo
addressValid = true
discord = users[0].User.ID
}
} else if emailEnabled {
addressValid = true
invite.SendTo = req.SendTo
}
if addressValid {
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
} else {
var err error
if discord != "" {
err = app.discord.SendDM(msg, discord)
} else {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
} else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
}
}
}
}
if req.Profile != "" {
if _, ok := app.storage.profiles[req.Profile]; ok {
invite.Profile = req.Profile
} else {
invite.Profile = "Default"
}
}
app.storage.invites[inviteCode] = invite
app.storage.storeInvites()
respondBool(200, true, gc)
}
// @Summary Get invites.
// @Produce json
// @Success 200 {object} getInvitesDTO
// @Router /invites [get]
// @Security Bearer
// @tags Invites
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
currentTime := time.Now()
app.storage.loadInvites()
app.checkInvites()
var invites []inviteDTO
for code, inv := range app.storage.invites {
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{
Code: code,
Months: months,
Days: days,
Hours: hours,
Minutes: minutes,
UserExpiry: inv.UserExpiry,
UserMonths: inv.UserMonths,
UserDays: inv.UserDays,
UserHours: inv.UserHours,
UserMinutes: inv.UserMinutes,
Created: inv.Created.Unix(),
Profile: inv.Profile,
NoLimit: inv.NoLimit,
Label: inv.Label,
}
if len(inv.UsedBy) != 0 {
invite.UsedBy = map[string]int64{}
for _, pair := range inv.UsedBy {
// These used to be stored formatted instead of as a unix timestamp.
unix, err := strconv.ParseInt(pair[1], 10, 64)
if err != nil {
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
if err != nil {
app.err.Printf("Failed to parse usedBy time: %v", err)
}
unix = date.Unix()
}
invite.UsedBy[pair[0]] = unix
}
}
invite.RemainingUses = 1
if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses
}
if inv.SendTo != "" {
invite.SendTo = inv.SendTo
}
if len(inv.Notify) != 0 {
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails()
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
address = addr.Addr
}
} else {
address = app.config.Section("ui").Key("email").String()
}
if _, ok := inv.Notify[address]; ok {
if _, ok = inv.Notify[address]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[address]["notify-expiry"]
}
if _, ok = inv.Notify[address]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[address]["notify-creation"]
}
}
}
invites = append(invites, invite)
}
profiles := make([]string, len(app.storage.profiles))
if len(app.storage.profiles) != 0 {
profiles[0] = app.storage.defaultProfile
i := 1
if len(app.storage.profiles) > 1 {
for p := range app.storage.profiles {
if p != app.storage.defaultProfile {
profiles[i] = p
i++
}
}
}
}
resp := getInvitesDTO{
Profiles: profiles,
Invites: invites,
}
gc.JSON(200, resp)
}
// @Summary Set profile for an invite
// @Produce json
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /invites/profile [post]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) SetProfile(gc *gin.Context) {
var req inviteProfileDTO
gc.BindJSON(&req)
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
// "" means "Don't apply profile"
if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" {
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
respond(500, "Profile not found", gc)
return
}
inv := app.storage.invites[req.Invite]
inv.Profile = req.Profile
app.storage.invites[req.Invite] = inv
app.storage.storeInvites()
respondBool(200, true, gc)
}
// @Summary Set notification preferences for an invite.
// @Produce json
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
// @Success 200
// @Failure 400 {object} stringResponse
// @Failure 500 {object} stringResponse
// @Router /invites/notify [post]
// @Security Bearer
// @tags Other
func (app *appContext) SetNotify(gc *gin.Context) {
var req map[string]map[string]bool
gc.BindJSON(&req)
changed := false
for code, settings := range req {
app.debug.Printf("%s: Notification settings change requested", code)
app.storage.loadInvites()
app.storage.loadEmails()
invite, ok := app.storage.invites[code]
if !ok {
app.err.Printf("%s Notification setting change failed: Invalid code", code)
respond(400, "Invalid invite code", gc)
return
}
var address string
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(false)
if jellyfinLogin {
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
if !addressAvailable {
app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user contact method", gc)
return
}
address = gc.GetString("jfId")
} else {
address = app.config.Section("ui").Key("email").String()
}
if invite.Notify == nil {
invite.Notify = map[string]map[string]bool{}
}
if _, ok := invite.Notify[address]; !ok {
invite.Notify[address] = map[string]bool{}
} /*else {
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
*/
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
changed = true
}
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
changed = true
}
if changed {
app.storage.invites[code] = invite
}
}
if changed {
app.storage.storeInvites()
}
}
// @Summary Delete an invite.
// @Produce json
// @Param deleteInviteDTO body deleteInviteDTO true "Delete invite object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Router /invites [delete]
// @Security Bearer
// @tags Invites
func (app *appContext) DeleteInvite(gc *gin.Context) {
var req deleteInviteDTO
gc.BindJSON(&req)
app.debug.Printf("%s: Deletion requested", req.Code)
var ok bool
_, ok = app.storage.invites[req.Code]
if ok {
delete(app.storage.invites, req.Code)
app.storage.storeInvites()
app.info.Printf("%s: Invite deleted", req.Code)
respondBool(200, true, gc)
return
}
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
respond(400, "Code doesn't exist", gc)
}

700
api-messages.go Normal file
View File

@ -0,0 +1,700 @@
package main
import (
"strings"
"time"
"github.com/gin-gonic/gin"
"gopkg.in/ini.v1"
)
// @Summary Get a list of email names and IDs.
// @Produce json
// @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")
if _, ok := app.storage.lang.Email[lang]; !ok {
lang = app.storage.lang.chosenEmailLang
}
gc.JSON(200, emailListDTO{
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled},
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.customEmails.UserDisabled.Enabled},
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.customEmails.UserEnabled.Enabled},
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled},
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled},
})
}
func (app *appContext) getCustomEmail(id string) *customEmail {
switch id {
case "Announcement":
return &customEmail{}
case "UserCreated":
return &app.storage.customEmails.UserCreated
case "InviteExpiry":
return &app.storage.customEmails.InviteExpiry
case "PasswordReset":
return &app.storage.customEmails.PasswordReset
case "UserDeleted":
return &app.storage.customEmails.UserDeleted
case "UserDisabled":
return &app.storage.customEmails.UserDisabled
case "UserEnabled":
return &app.storage.customEmails.UserEnabled
case "InviteEmail":
return &app.storage.customEmails.InviteEmail
case "WelcomeEmail":
return &app.storage.customEmails.WelcomeEmail
case "EmailConfirmation":
return &app.storage.customEmails.EmailConfirmation
case "UserExpired":
return &app.storage.customEmails.UserExpired
}
return nil
}
// @Summary Sets the corresponding custom email.
// @Produce json
// @Param customEmail body customEmail true "Content = email (in markdown)."
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @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
gc.BindJSON(&req)
id := gc.Param("id")
if req.Content == "" {
respondBool(400, false, gc)
return
}
email := app.getCustomEmail(id)
if email == nil {
respondBool(400, false, gc)
return
}
email.Content = req.Content
email.Enabled = true
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Enable/Disable custom email.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @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")
s := gc.Param("state")
enabled := false
if s == "enable" {
enabled = true
} else if s != "disable" {
respondBool(400, false, gc)
}
email := app.getCustomEmail(id)
if email == nil {
respondBool(400, false, gc)
return
}
email.Enabled = enabled
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
// @Produce json
// @Success 200 {object} customEmailDTO
// @Failure 400 {object} boolResponse
// @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 *Message
var variables []string
var conditionals []string
var values map[string]interface{}
username := app.storage.lang.Email[lang].Strings.get("username")
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
email := app.getCustomEmail(id)
if email == nil {
app.err.Printf("Failed to get custom email with ID \"%s\"", id)
respondBool(400, false, gc)
return
}
if id == "WelcomeEmail" {
conditionals = []string{"{yourAccountWillExpire}"}
email.Conditionals = conditionals
}
content = email.Content
noContent := content == ""
if !noContent {
variables = email.Variables
}
switch id {
case "Announcement":
// Just send the email html
content = ""
case "UserCreated":
if noContent {
msg, err = app.email.constructCreated("", "", "", Invite{}, app, true)
}
values = app.email.createdValues("xxxxxx", username, emailAddress, Invite{}, app, false)
case "InviteExpiry":
if noContent {
msg, err = app.email.constructExpiry("", Invite{}, app, true)
}
values = app.email.expiryValues("xxxxxx", Invite{}, app, false)
case "PasswordReset":
if noContent {
msg, err = app.email.constructReset(PasswordReset{}, app, true)
}
values = app.email.resetValues(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
case "UserDeleted":
if noContent {
msg, err = app.email.constructDeleted("", app, true)
}
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
case "UserDisabled":
if noContent {
msg, err = app.email.constructDisabled("", app, true)
}
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
case "UserEnabled":
if noContent {
msg, err = app.email.constructEnabled("", app, true)
}
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
case "InviteEmail":
if noContent {
msg, err = app.email.constructInvite("", Invite{}, app, true)
}
values = app.email.inviteValues("xxxxxx", Invite{}, app, false)
case "WelcomeEmail":
if noContent {
msg, err = app.email.constructWelcome("", time.Time{}, app, true)
}
values = app.email.welcomeValues(username, time.Now(), app, false, true)
case "EmailConfirmation":
if noContent {
msg, err = app.email.constructConfirmation("", "", "", app, true)
}
values = app.email.confirmationValues("xxxxxx", username, "xxxxxx", app, false)
case "UserExpired":
if noContent {
msg, err = app.email.constructUserExpired(app, true)
}
values = app.email.userExpiredValues(app, false)
}
if err != nil {
respondBool(500, false, gc)
return
}
if noContent && id != "Announcement" {
content = msg.Text
variables = make([]string, strings.Count(content, "{"))
i := 0
found := false
buf := ""
for _, c := range content {
if !found && c != '{' && c != '}' {
continue
}
found = true
buf += string(c)
if c == '}' {
found = false
variables[i] = buf
buf = ""
i++
}
}
email.Variables = variables
}
if variables == nil {
variables = []string{}
}
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
mail, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
if err != nil {
respondBool(500, false, gc)
return
}
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
}
// @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]
}
linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc)
}
// @Summary Sets whether to notify a user through telegram/discord/matrix/email or not.
// @Produce json
// @Param SetContactMethodsDTO body SetContactMethodsDTO 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) SetContactMethods(gc *gin.Context) {
var req SetContactMethodsDTO
gc.BindJSON(&req)
if req.ID == "" {
respondBool(400, false, gc)
return
}
if tgUser, ok := app.storage.telegram[req.ID]; ok {
change := tgUser.Contact != req.Telegram
tgUser.Contact = req.Telegram
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
}
if change {
msg := ""
if !req.Telegram {
msg = " not"
}
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
}
}
if dcUser, ok := app.storage.discord[req.ID]; ok {
change := dcUser.Contact != req.Discord
dcUser.Contact = req.Discord
app.storage.discord[req.ID] = dcUser
if err := app.storage.storeDiscordUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Discord: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Discord {
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
}
}
if mxUser, ok := app.storage.matrix[req.ID]; ok {
change := mxUser.Contact != req.Matrix
mxUser.Contact = req.Matrix
app.storage.matrix[req.ID] = mxUser
if err := app.storage.storeMatrixUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Matrix: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Matrix {
msg = " not"
}
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
}
}
if email, ok := app.storage.emails[req.ID]; ok {
change := email.Contact != req.Email
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
if change {
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
}
respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
// @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 Returns true/false on whether or not a discord PIN was verified. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/verified/{pin} [get]
// @tags Other
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
pin := gc.Param("pin")
_, ok := app.discord.verifiedTokens[pin]
respondBool(200, ok, gc)
}
// @Summary Returns a 10-minute, one-use Discord server invite
// @Produce json
// @Success 200 {object} DiscordInviteDTO
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/invite [get]
// @tags Other
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
if app.discord.inviteChannelName == "" {
respondBool(400, false, gc)
return
}
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
if invURL == "" {
respondBool(500, false, gc)
return
}
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
}
// @Summary Generate and send a new PIN to a specified Matrix user.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code"
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
// @Router /invite/{invCode}/matrix/user [post]
// @tags Other
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
var req MatrixSendPINDTO
gc.BindJSON(&req)
if req.UserID == "" {
respondBool(400, false, gc)
return
}
ok := app.matrix.SendStart(req.UserID)
if !ok {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Param userID path string true "Matrix User ID"
// @Router /invite/{invCode}/matrix/verified/{userID}/{pin} [get]
// @tags Other
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
app.debug.Println("Matrix: Invite code was invalid")
respondBool(401, false, gc)
return
}
userID := gc.Param("userID")
pin := gc.Param("pin")
user, ok := app.matrix.tokens[pin]
if !ok {
app.debug.Println("Matrix: PIN not found")
respondBool(200, false, gc)
return
}
if user.User.UserID != userID {
app.debug.Println("Matrix: User ID of PIN didn't match")
respondBool(200, false, gc)
return
}
user.Verified = true
app.matrix.tokens[pin] = user
respondBool(200, true, gc)
}
// @Summary Generates a Matrix access token from a username and password.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
// @Router /matrix/login [post]
// @tags Other
func (app *appContext) MatrixLogin(gc *gin.Context) {
var req MatrixLoginDTO
gc.BindJSON(&req)
if req.Username == "" || req.Password == "" {
respond(400, "errorLoginBlank", gc)
return
}
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
if err != nil {
app.err.Printf("Matrix: Failed to generate token: %v", err)
respond(401, "Unauthorized", gc)
return
}
tempConfig, _ := ini.Load(app.configPath)
matrix := tempConfig.Section("matrix")
matrix.Key("enabled").SetValue("true")
matrix.Key("homeserver").SetValue(req.Homeserver)
matrix.Key("token").SetValue(token)
matrix.Key("user_id").SetValue(req.Username)
if err := tempConfig.SaveTo(app.configPath); err != nil {
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Links a Matrix user to a Jellyfin account via user IDs. Notifications are turned on by default.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
// @Router /users/matrix [post]
// @tags Other
func (app *appContext) MatrixConnect(gc *gin.Context) {
var req MatrixConnectUserDTO
gc.BindJSON(&req)
if app.storage.matrix == nil {
app.storage.matrix = map[string]MatrixUser{}
}
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
if err != nil {
app.err.Printf("Matrix: Failed to create room: %v", err)
respondBool(500, false, gc)
return
}
app.storage.matrix[req.JellyfinID] = MatrixUser{
UserID: req.UserID,
RoomID: string(roomID),
Lang: "en-us",
Contact: true,
Encrypted: encrypted,
}
app.matrix.isEncrypted[roomID] = encrypted
if err := app.storage.storeMatrixUsers(); err != nil {
app.err.Printf("Failed to store Matrix users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
// @Produce json
// @Success 200 {object} DiscordUsersDTO
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param username path string true "username to search."
// @Router /users/discord/{username} [get]
// @tags Other
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
name := gc.Param("username")
if name == "" {
respondBool(400, false, gc)
return
}
users := app.discord.GetUsers(name)
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
for i, u := range users {
resp.Users[i] = DiscordUserDTO{
Name: u.User.Username + "#" + u.User.Discriminator,
ID: u.User.ID,
AvatarURL: u.User.AvatarURL("32"),
}
}
gc.JSON(200, resp)
}
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
// @Router /users/discord [post]
// @tags Other
func (app *appContext) DiscordConnect(gc *gin.Context) {
var req DiscordConnectUserDTO
gc.BindJSON(&req)
if req.JellyfinID == "" || req.DiscordID == "" {
respondBool(400, false, gc)
return
}
user, ok := app.discord.NewUser(req.DiscordID)
if !ok {
respondBool(500, false, gc)
return
}
app.storage.discord[req.JellyfinID] = user
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
respondBool(500, false, gc)
return
}
linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc)
}

119
api-ombi.go Normal file
View File

@ -0,0 +1,119 @@
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
ombiUsers, code, err := app.ombi.GetUsers()
if err != nil || code != 200 {
return nil, code, err
}
jfUser, code, err := app.jf.UserByID(jfID, false)
if err != nil || code != 200 {
return nil, code, err
}
username := jfUser.Name
email := ""
if e, ok := app.storage.emails[jfID]; ok {
email = e.Addr
}
for _, ombiUser := range ombiUsers {
ombiAddr := ""
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
ombiAddr = a.(string)
}
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
return ombiUser, code, err
}
}
return nil, 400, fmt.Errorf("Couldn't find user")
}
// @Summary Get a list of Ombi users.
// @Produce json
// @Success 200 {object} ombiUsersDTO
// @Failure 500 {object} stringResponse
// @Router /ombi/users [get]
// @Security Bearer
// @tags Ombi
func (app *appContext) OmbiUsers(gc *gin.Context) {
app.debug.Println("Ombi users requested")
users, status, err := app.ombi.GetUsers()
if err != nil || status != 200 {
app.err.Printf("Failed to get users from Ombi (%d): %v", status, err)
respond(500, "Couldn't get users", gc)
return
}
userlist := make([]ombiUser, len(users))
for i, data := range users {
userlist[i] = ombiUser{
Name: data["userName"].(string),
ID: data["id"].(string),
}
}
gc.JSON(200, ombiUsersDTO{Users: userlist})
}
// @Summary Store Ombi user template in an existing profile.
// @Produce json
// @Param ombiUser body ombiUser true "User to source settings from"
// @Param profile path string true "Name of profile to store in"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/ombi/{profile} [post]
// @Security Bearer
// @tags Ombi
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
var req ombiUser
gc.BindJSON(&req)
profileName := gc.Param("profile")
profile, ok := app.storage.profiles[profileName]
if !ok {
respondBool(400, false, gc)
return
}
template, code, err := app.ombi.TemplateByID(req.ID)
if err != nil || code != 200 || len(template) == 0 {
app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err)
respond(500, "Couldn't get user", gc)
return
}
profile.Ombi = template
app.storage.profiles[profileName] = profile
if err := app.storage.storeProfiles(); err != nil {
respond(500, "Failed to store profile", gc)
app.err.Printf("Failed to store profiles: %v", err)
return
}
respondBool(204, true, gc)
}
// @Summary Remove ombi user template from a profile.
// @Produce json
// @Param profile path string true "Name of profile to store in"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/ombi/{profile} [delete]
// @Security Bearer
// @tags Ombi
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
profileName := gc.Param("profile")
profile, ok := app.storage.profiles[profileName]
if !ok {
respondBool(400, false, gc)
return
}
profile.Ombi = nil
app.storage.profiles[profileName] = profile
if err := app.storage.storeProfiles(); err != nil {
respond(500, "Failed to store profile", gc)
app.err.Printf("Failed to store profiles: %v", err)
return
}
respondBool(204, true, gc)
}

121
api-profiles.go Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"time"
"github.com/gin-gonic/gin"
)
// @Summary Get a list of profiles
// @Produce json
// @Success 200 {object} getProfilesDTO
// @Router /profiles [get]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) GetProfiles(gc *gin.Context) {
app.storage.loadProfiles()
app.debug.Println("Profiles requested")
out := getProfilesDTO{
DefaultProfile: app.storage.defaultProfile,
Profiles: map[string]profileDTO{},
}
for name, p := range app.storage.profiles {
out.Profiles[name] = profileDTO{
Admin: p.Admin,
LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser,
Ombi: p.Ombi != nil,
}
}
gc.JSON(200, out)
}
// @Summary Set the default profile to use.
// @Produce json
// @Param profileChangeDTO body profileChangeDTO true "Default profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/default [post]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
req := profileChangeDTO{}
gc.BindJSON(&req)
app.info.Printf("Setting default profile to \"%s\"", req.Name)
if _, ok := app.storage.profiles[req.Name]; !ok {
app.err.Printf("Profile not found: \"%s\"", req.Name)
respond(500, "Profile not found", gc)
return
}
for name, profile := range app.storage.profiles {
if name == req.Name {
profile.Admin = true
app.storage.profiles[name] = profile
} else {
profile.Admin = false
}
}
app.storage.defaultProfile = req.Name
respondBool(200, true, gc)
}
// @Summary Create a profile based on a Jellyfin user's settings.
// @Produce json
// @Param newProfileDTO body newProfileDTO true "New profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles [post]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) CreateProfile(gc *gin.Context) {
app.info.Println("Profile creation requested")
var req newProfileDTO
gc.BindJSON(&req)
app.jf.CacheExpiry = time.Now()
user, status, err := app.jf.UserByID(req.ID, false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err)
respond(500, "Couldn't get user", gc)
return
}
profile := Profile{
FromUser: user.Name,
Policy: user.Policy,
}
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
if req.Homescreen {
profile.Configuration = user.Configuration
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err)
respond(500, "Couldn't get displayprefs", gc)
return
}
}
app.storage.loadProfiles()
app.storage.profiles[req.Name] = profile
app.storage.storeProfiles()
app.storage.loadProfiles()
respondBool(200, true, gc)
}
// @Summary Delete an existing profile
// @Produce json
// @Param profileChangeDTO body profileChangeDTO true "Delete profile object"
// @Success 200 {object} boolResponse
// @Router /profiles [delete]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) DeleteProfile(gc *gin.Context) {
req := profileChangeDTO{}
gc.BindJSON(&req)
name := req.Name
if _, ok := app.storage.profiles[name]; ok {
if app.storage.defaultProfile == name {
app.storage.defaultProfile = ""
}
delete(app.storage.profiles, name)
}
app.storage.storeProfiles()
respondBool(200, true, gc)
}

1140
api-users.go Normal file

File diff suppressed because it is too large Load Diff

2470
api.go

File diff suppressed because it is too large Load Diff

12
go.mod
View File

@ -12,6 +12,8 @@ replace github.com/hrfee/jfa-go/logger => ./logger
replace github.com/hrfee/jfa-go/linecache => ./linecache
replace github.com/hrfee/jfa-go/api => ./api
require (
github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
@ -24,6 +26,7 @@ require (
github.com/gin-contrib/pprof v1.3.0
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.7.7
github.com/go-openapi/swag v0.21.1 // indirect
github.com/go-playground/validator/v10 v10.9.0 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
@ -49,16 +52,17 @@ require (
github.com/steambap/captcha v1.4.1
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2
github.com/swaggo/gin-swagger v1.3.3
github.com/swaggo/swag v1.7.8 // indirect
github.com/swaggo/swag v1.8.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tidwall/sjson v1.2.4 // indirect
github.com/ugorji/go v1.2.6 // indirect
github.com/urfave/cli/v2 v2.4.0 // indirect
github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/xhit/go-simple-mail/v2 v2.10.0
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/tools v0.1.8 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
golang.org/x/tools v0.1.10 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.66.2
maunium.net/go/mautrix v0.10.7

16
go.sum
View File

@ -24,6 +24,8 @@ github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820 h1:MIW5DnBVJ
github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -104,6 +106,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@ -257,6 +261,8 @@ github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG
github.com/swaggo/swag v1.7.4/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI=
github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
github.com/swaggo/swag v1.8.0 h1:80NNhvpJcuItNpBDqgJwDuKlMmaZ/OATOzhG3bhcM3w=
github.com/swaggo/swag v1.8.0/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@ -284,6 +290,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
@ -308,6 +316,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -328,6 +338,8 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba h1:6u6sik+bn/y7vILcYkK3iwTBWN7WtBvB0+SZswQnbf8=
golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -358,6 +370,8 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -377,6 +391,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -43,7 +43,7 @@
"settingsRequiredOrRestartMessage": "Hinweis: {n} zeigt ein erforderliches Feld an, {n} zeigt an, dass Änderungen einen Neustart erfordern.",
"settingsSave": "Speichern",
"ombiUserDefaults": "Ombi-Benutzerstandardeinstellungen",
"ombiUserDefaultsDescription": "Erstelle einen Ombi-Benutzer und konfiguriere ihn, dann wähle ihn unten aus. Seine Einstellungen/Berechtigungen werden gespeichert und auf neue Ombi-Benutzer, erstellt von jfa-go, angewendet",
"ombiUserDefaultsDescription": "Erstelle einen Ombi-Benutzer, konfiguriere ihn und wähle ihn dann unten aus. Seine Einstellungen/Berechtigungen werden gespeichert und auf neue Ombi-Benutzer, welche von jfa-go erstellt werden, angewendet, sofern dieses Profil ausgewählt ist.",
"userProfiles": "Benutzerprofile",
"userProfilesDescription": "Profile werden auf Benutzer angewendet, wenn sie ein Konto erstellen. Ein Profil beinhaltet Bibliothekszugriffsrechte und das Startbildschirmlayout.",
"userProfilesIsDefault": "Standard",
@ -69,7 +69,7 @@
"preview": "Vorschau",
"reset": "Zurücksetzen",
"edit": "Bearbeiten",
"customizeMessages": "E-Mails anpassen",
"customizeMessages": "Benachrichtigungen 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": "Betreff",
@ -101,7 +101,20 @@
"findDiscordUser": "Suche Discord-Benutzer",
"linkMatrixDescription": "Gib den Benutzernamen und das Passwort des Benutzers ein, der als Bot verwendet werden soll. Nach dem Absenden wird die App neu gestartet.",
"matrixHomeServer": "Adresse des Homeservers",
"templates": "Vorlagen"
"templates": "Vorlagen",
"ombiProfile": "Ombi-Benutzerprofil",
"accessJFA": "jfa-go Zugriff",
"sendPWRValidFor": "Der Link ist 30m gültig.",
"logs": "Logdaten",
"setExpiry": "Ablauf setzen",
"sendPWRSuccess": "Link zur Passwortrücksetzung versandt.",
"sendPWRSuccessManual": "Falls der Benutzer ihn nicht erhalten hat, klicke \"Kopieren\" und sende ihm den Link manuell.",
"sendPWR": "Sende Passwortrücksetzung",
"sendPWRManual": "Benutzer {n} hat keine Kontaktmöglichkeit hinterlegt. Klicke \"Kopieren\" um einen Link zu erhalten, den du dem Benutzer manuell senden kannst.",
"accessJFASettings": "Kann nicht geändert werden, da entweder \"Nur Admin-Benutzer\" oder \"Erlaube allen Jellyfin-Nutzern sich anzumelden\" in Einstellungen > Allgemein aktiviert ist.",
"saveAsTemplate": "Als Vorlage speichern",
"deleteTemplate": "Vorlage löschen",
"templateEnterName": "Gebe einen Namen ein, um diese Vorlage zu speichern."
},
"notifications": {
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
@ -142,7 +155,9 @@
"updateAppliedRefresh": "Update angewendet, bitte aktualisieren.",
"telegramVerified": "Telegram-Konto verifiziert.",
"accountConnected": "Konto verbunden.",
"savedAnnouncement": "Ankündigung gespeichert."
"savedAnnouncement": "Ankündigung gespeichert.",
"errorSetOmbiProfile": "Ombi-Profil konnte nicht gespeichert werden.",
"setOmbiProfile": "Ombi-Profil gespeichert."
},
"quantityStrings": {
"modifySettingsFor": {
@ -196,6 +211,10 @@
"reEnableUsers": {
"singular": "Benutzer {n} wieder aktivieren",
"plural": "Benutzer {n} wieder aktivieren"
},
"setExpiry": {
"singular": "Ablauf für {n} Benutzer setzen",
"plural": "Ablauf für {n} Benutzer setzen"
}
}
}

View File

@ -113,7 +113,7 @@
"deleteTemplate": "Delete template",
"templateEnterName": "Enter a name to save this template.",
"accessJFA": "Access jfa-go",
"accessJFASettings": "Cannot be changed as this either \"Admin Only\" or \"Allow All\" has been set in Settings > General."
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General."
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",

View File

@ -112,7 +112,9 @@
"sendPWRSuccess": "Wachtwoordreset-link verstuurd.",
"sendPWRValidFor": "De link is 30m geldig.",
"ombiProfile": "Ombi gebruikersprofiel",
"logs": "Logs"
"logs": "Logs",
"accessJFA": "Toegang tot jfa-go",
"accessJFASettings": "Kan niet worden aangepast, omdat \"Alleen beheerders\" of \"Laat alle Jellyfin-gebruikers inloggen\" is aangevinkt in Instellingen > Algemeen."
},
"notifications": {
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
@ -209,6 +211,10 @@
"enabledUser": {
"singular": "{n} gebruiker ingeschakeld.",
"plural": "{n} gebruikers ingeschakeld."
},
"setExpiry": {
"singular": "Stel verloop in voor {n} gebruiker",
"plural": "Stel verloop in voor {n} gebruikers"
}
}
}

View File

@ -22,6 +22,8 @@
"linkDiscord": "Link Discord",
"linkMatrix": "Link Matrix",
"send": "Senden",
"contactDiscord": "Kontakt über Discord"
"contactDiscord": "Kontakt über Discord",
"refresh": "Aktualisieren",
"required": "Erforderlich"
}
}

View File

@ -22,6 +22,8 @@
"send": "Verstuur",
"linkDiscord": "Koppel Discord",
"linkMatrix": "Koppel Matrix",
"contactDiscord": "Stuur Discord bericht"
"contactDiscord": "Stuur Discord bericht",
"refresh": "Ververs",
"required": "Verplicht"
}
}

29
lang/common/sl-si.json Normal file
View File

@ -0,0 +1,29 @@
{
"meta": {
"name": "Angleščina (ZDA)"
},
"strings": {
"username": "Uporabniško ime",
"password": "Geslo",
"emailAddress": "E-poštni naslov",
"name": "Ime",
"submit": "Potrdi",
"send": "Pošlji",
"success": "Uspeh",
"continue": "Nadaljuj",
"error": "Napaka",
"copy": "Kopiraj",
"copied": "Kopirano",
"time24h": "24ur format čas",
"time12h": "12ur format časa",
"linkTelegram": "Poveži Telegram",
"contactEmail": "Kontakt preko e-pošte",
"contactTelegram": "Kontakt preko Telegrama",
"linkDiscord": "Poveži Discord",
"linkMatrix": "Poveži Matrix",
"contactDiscord": "Kontakt preko Discorda",
"theme": "Tema",
"refresh": "Osveži",
"required": "Obvezno"
}
}

View File

@ -11,14 +11,14 @@
"title": "Mitteilung: Benutzer erstellt",
"aUserWasCreated": "Ein Benutzer wurde unter Verwendung des Codes {code} erstellt.",
"time": "Zeit",
"notificationNotice": "Hinweis: Benachrichtigungs-E-Mails können auf dem Administrator-Dashboard umgeschalten werden.",
"notificationNotice": "Hinweis: Benachrichtigungen können auf dem Administrator-Dashboard umgeschalten werden.",
"name": "Benutzererstellung"
},
"inviteExpiry": {
"title": "Mitteilung: Invite abgelaufen",
"inviteExpired": "Invite abgelaufen.",
"expiredAt": "Code {code} lief um {time} ab.",
"notificationNotice": "Hinweis: Benachrichtigungs-E-Mails können auf dem Administrator-Dashboard umgeschalten werden.",
"notificationNotice": "Hinweis: Benachrichtigungen können auf dem Administrator-Dashboard umgeschalten werden.",
"name": "Invite Ablaufdatum"
},
"passwordReset": {

60
lang/form/ar-aa.json Normal file
View File

@ -0,0 +1,60 @@
{
"meta": {
"name": "العربية (AR)"
},
"strings": {
"pageTitle": "أنشاء حساب",
"createAccountHeader": "أنشاء حساب",
"accountDetails": "التفاصيل",
"emailAddress": "البريد الالكتروني",
"username": "اسم المستخدم",
"password": "كلمة المرور",
"reEnterPassword": "تأكيد كلمة المرور",
"reEnterPasswordInvalid": "كلمة المرور غير مطابقة.",
"createAccountButton": "أنشاء حساب",
"passwordRequirementsHeader": "متطلبات كلمة المرور",
"successHeader": "تم!",
"confirmationRequired": "مطلوب تأكيد البريد الإلكتروني",
"confirmationRequiredMessage": "يرجى التحقق من صندوق البريد الإلكتروني الخاص بك للتحقق من عنوانك.",
"yourAccountIsValidUntil": "سيكون حسابك ساري المفعول حتى {date}.",
"sendPIN": "أرسل رقم التعريف الشخصي أدناه إلى الروبوت ، ثم ارجع إلى هنا لربط حسابك.",
"sendPINDiscord": "اكتب {command} في {server_channel} على Discord ، ثم أرسل رقم التعريف الشخصي أدناه.",
"matrixEnterUser": "أدخل معرف المستخدم الخاص بك ، واضغط على إرسال ، وسيتم إرسال رمز PIN إليك. أدخله هنا للمتابعة."
},
"notifications": {
"errorUserExists": "المستخدم موجود مسبقا.",
"errorInvalidCode": "رمز دعوة غير صالح.",
"errorTelegramVerification": "مطلوب التحقق من Telegram.",
"errorDiscordVerification": "مطلوب التحقق من الخلاف.",
"errorMatrixVerification": "مطلوب التحقق من المصفوفة.",
"errorInvalidPIN": "رقم التعريف الشخصي غير صالح.",
"errorUnknown": "خطأ غير معروف.",
"errorNoEmail": "البريد الإلكتروني مطلوب.",
"errorCaptcha": "كلمة التحقق غير صحيحة.",
"errorPassword": "تحقق من متطلبات كلمة المرور.",
"errorNoMatch": "كلمات المرور غير متطابقة.",
"verified": "تم التحقق من الحساب."
},
"validationStrings": {
"length": {
"singular": "يجب أن يتألف من {n} حرف على الأقل",
"plural": "يجب ألا يقل عدد الأحرف عن {n}"
},
"uppercase": {
"singular": "يجب أن تحتوي على {n} حرف كبير على الأقل",
"plural": "يجب ألا يقل عدد الأحرف الكبيرة عن {n}"
},
"lowercase": {
"singular": "يجب أن يتألف من {n} حرف صغير على الأقل",
"plural": "يجب ألا يقل عدد الأحرف الصغيرة عن {n}"
},
"number": {
"singular": "يجب أن يحتوي على {n} رقم على الأقل",
"plural": "يجب أن يحتوي على {n} رقم على الأقل"
},
"special": {
"singular": "يجب أن يتألف من {n} حرف خاص على الأقل",
"plural": "يجب أن يتألف من {n} حرف خاص على الأقل"
}
}
}

View File

@ -15,10 +15,10 @@
"passwordRequirementsHeader": "Passwortanforderungen",
"successHeader": "Erfolgreich!",
"confirmationRequired": "E-Mail-Bestätigung erforderlich",
"confirmationRequiredMessage": "Bitte überprüfe dein Posteingang und bestätige deine E-Mail-Adresse.",
"confirmationRequiredMessage": "Bitte überprüfe deinen Posteingang und bestätige deine E-Mail-Adresse.",
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein.",
"sendPIN": "Sende die untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden.",
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN als DM an den Bot.",
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN.",
"matrixEnterUser": "Gib deine Benutzer-ID ein und drücke auf Absenden. Anschließend erhälst du ein PIN, die hier eingegeben wird um fortzufahren."
},
"validationStrings": {
@ -53,6 +53,9 @@
"errorMatrixVerification": "Matrix-Verifizierung erforderlich.",
"errorUnknown": "Unbekannter Fehler.",
"verified": "Konto verifiziert.",
"errorNoEmail": "E-Mail Adresse erforderlich."
"errorNoEmail": "E-Mail Adresse erforderlich.",
"errorCaptcha": "Captcha falsch.",
"errorPassword": "Prüfe die Passwortanforderungen.",
"errorNoMatch": "Passwörter stimmen nicht überein."
}
}

View File

@ -19,7 +19,7 @@
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}.",
"sendPIN": "Stuur onderstaande pincode naar de bot, en kom daarna hier terug om je account te koppelen.",
"matrixEnterUser": "Voer je gebruikers ID in, druk op versturen, en er wordt je een pincode toegestuurd. Vul die hier in om door te gaan.",
"sendPINDiscord": "Typ {command} in {server_channel} op Discord, stuur daarna onderstaande pincode via DM naar de bot."
"sendPINDiscord": "Typ {command} in {server_channel} op Discord, stuur daarna onderstaande pincode."
},
"validationStrings": {
"length": {
@ -53,6 +53,9 @@
"errorUnknown": "Onbekende fout.",
"errorMatrixVerification": "Matrix-verificatie vereist.",
"verified": "Account geverifieerd.",
"errorNoEmail": "E-mail vereist."
"errorNoEmail": "E-mail vereist.",
"errorPassword": "Controleer wachtwoordeisen.",
"errorNoMatch": "Wachtwoorden komen niet overeen.",
"errorCaptcha": "Captcha incorrect."
}
}

60
lang/form/sl-si.json Normal file
View File

@ -0,0 +1,60 @@
{
"meta": {
"name": "Angleščina (ZDA)"
},
"strings": {
"pageTitle": "Ustvari Jellyfin Račun",
"createAccountHeader": "Ustvari Račun",
"accountDetails": "Podrobnosti",
"emailAddress": "E-pošta",
"username": "Uporabniško ime",
"password": "Geslo",
"reEnterPassword": "Ponovno vnesi Geslo",
"reEnterPasswordInvalid": "Gesli se ne ujemata.",
"createAccountButton": "Ustvari Račun",
"passwordRequirementsHeader": "Zahteve za geslo",
"successHeader": "Uspeh!",
"confirmationRequired": "Potrebna potrditev e-pošte",
"confirmationRequiredMessage": "Prosimo preverite svojo e-pošto da potrdite račun.",
"yourAccountIsValidUntil": "Vaš račun bo veljaven do {date}.",
"sendPIN": "Pošljite spodnji PIN botu, nato pa pridite nazaj da povežete svoj račun.",
"sendPINDiscord": "Napišite {command} v {server_channel} na Discordu, nato pa vnesite PIN spodaj.",
"matrixEnterUser": "Vnesite svoj uporabniški ID, pritisnite potrdi, in poslan vam bo PIN. Tega vnesite tu, da nadaljujete."
},
"notifications": {
"errorUserExists": "Uporabnik že obstaja.",
"errorInvalidCode": "Neveljavna koda povabila.",
"errorTelegramVerification": "Potrebna je potrditev preko telegrama.",
"errorDiscordVerification": "Potrebna je potrditev preko Discorda.",
"errorMatrixVerification": "Potrebna je potrditev preko Matrix.",
"errorInvalidPIN": "PIN ni veljaven.",
"errorUnknown": "Neznana napaka.",
"errorNoEmail": "E-pošta obvezna.",
"errorCaptcha": "Captcha je nepravilna.",
"errorPassword": "Prosimo preverite zahteve za geslo.",
"errorNoMatch": "Gesli se ne ujemata.",
"verified": "Račun potrjen."
},
"validationStrings": {
"length": {
"singular": "Potrebnih je vsaj {n} znakov",
"plural": "Potrebnih je vsaj {n} znakov"
},
"uppercase": {
"singular": "Potreben je vsaj {n} velik znak",
"plural": "Potrebnih je vsaj {n} velikih znakov"
},
"lowercase": {
"singular": "Potreben je vsaj {n} majhen znak",
"plural": "Potrebnih je vsaj {n} majhnih znakov"
},
"number": {
"singular": "Potrebna je vsaj {n} števka",
"plural": "Potrebnih je vsaj {n} števk"
},
"special": {
"singular": "Poteben je vsaj {n} poseben znak",
"plural": "Potrebnih je vsaj {n} posebnih znakov"
}
}
}

16
lang/pwreset/ar-aa.json Normal file
View File

@ -0,0 +1,16 @@
{
"meta": {
"name": "العربية (AR)"
},
"strings": {
"passwordReset": "إعادة تعيين كلمة المرور",
"reset": "إعادة تعيين",
"resetFailed": "فشل إعادة تعيين كلمة المرور",
"tryAgain": "حاول مرة اخرى.",
"youCanLogin": "يمكنك الآن تسجيل الدخول باستخدام الرمز أدناه ككلمة المرور الخاصة بك.",
"youCanLoginOmbi": "يمكنك الآن تسجيل الدخول باستخدام الرمز أدناه ككلمة المرور الخاصة بك.",
"youCanLoginPassword": "يمكنك الآن تسجيل الدخول باستخدام الرمز أدناه ككلمة المرور الخاصة بك.",
"changeYourPassword": "تأكد من تغيير كلمة المرور الخاصة بك بعد تسجيل الدخول.",
"enterYourPassword": "أدخل كلمة المرور الجديدة أدناه."
}
}

View File

@ -8,6 +8,9 @@
"tryAgain": "Bitte versuche es erneut.",
"youCanLogin": "Du kannst dich nun mit dem unten stehenden Code als Passwort anmelden.",
"youCanLoginOmbi": "Du kannst dich jetzt bei Jellyfin und Ombi mit dem unten stehenden Code als Passwort anmelden.",
"changeYourPassword": "Achte darauf, dass du dein Passwort nach der Anmeldung änderst."
"changeYourPassword": "Achte darauf, dass du dein Passwort nach der Anmeldung änderst.",
"youCanLoginPassword": "Du kannst dich nun mit deinem neuen Passwort anmelden. Klicke unten um zu Jellyfin zu gehen.",
"reset": "Zurücksetzen",
"enterYourPassword": "Gib unten dein neues Passwort ein."
}
}

16
lang/pwreset/sl-si.json Normal file
View File

@ -0,0 +1,16 @@
{
"meta": {
"name": "Angleščina (ZDA)"
},
"strings": {
"passwordReset": "Ponastavitev gesla",
"reset": "Ponastavi",
"resetFailed": "Ponastavitev gesla neuspešna",
"tryAgain": "Prosimo poskusite ponovno.",
"youCanLogin": "Sedaj se lahko prijavite s spodnjo kodo kot svojim geslom.",
"youCanLoginOmbi": "Sedaj se lahko prijavite v Jellyfin in Ombi s spodnjo kodo kot svojim geslom.",
"youCanLoginPassword": "Sedaj se lahko prijavite s svojim novim geslom. Pritisnite spodnji gumb da nadaljujete na Jellyfin.",
"changeYourPassword": "Spremenite svoje geslo po prijavi.",
"enterYourPassword": "Vnesite svoje novo geslo spodaj."
}
}

View File

@ -15,7 +15,11 @@
"serverAddress": "Serveradresse",
"emailSubject": "E-Mail-Betreff",
"URL": "URL",
"apiKey": "API-Schlüssel"
"apiKey": "API-Schlüssel",
"errorInvalidUserPass": "Benutzername/Passwort ungültig.",
"errorNotAdmin": "Dieser Benutzer darf den Server nicht verwalten.",
"errorUserDisabled": "Konto ist eventuell deaktiviert.",
"error404": "404, prüfe die interne URL."
},
"startPage": {
"welcome": "Willkommen!",
@ -25,7 +29,7 @@
},
"endPage": {
"finished": "Fertig!",
"restartMessage": "Es gibt weitere Einstellungen, die du auf der Admin-Seite konfigurieren kannst. Klicke unten, um neu zu starten, dann aktualisiere die Seite.",
"restartMessage": "In den Einstellungen kannst du Discord/Telegram/Matrix Bots konfigurieren, Benachrichtigungen anpassen und vieles mehr. Klicke unten, um neu zu starten, aktualisiere dann die Seite.",
"refreshPage": "Aktualisieren"
},
"language": {
@ -54,7 +58,9 @@
"authorizeWithJellyfin": "Mit Jellyfin/Emby autorisieren: Anmeldedaten werden mit Jellyfin geteilt, was mehrere Benutzer ermöglicht.",
"authorizeManual": "Benutzername und Passwort: Lege Benutzername und Passwort manuell fest.",
"adminOnly": "Nur Admin-Benutzer (empfohlen)",
"emailNotice": "Deine E-Mail-Adresse kann verwendet werden, um Benachrichtigungen zu erhalten."
"emailNotice": "Deine E-Mail-Adresse kann verwendet werden, um Benachrichtigungen zu erhalten.",
"allowAll": "Erlaube allen Jellyfin-Nutzern sich anzumelden",
"allowAllDescription": "Nicht empfohlen. Nach der Einrichtung solltest du einzelnen Benutzern erlauben, sich anzumelden."
},
"jellyfinEmby": {
"title": "Jellyfin/Emby",
@ -86,16 +92,16 @@
"mailgunApiURL": "API-URL"
},
"notifications": {
"title": "Benachrichtigungen",
"description": "Wenn aktiviert, kannst du (pro Invite) wählen, eine E-Mail zu erhalten, wenn ein Invite abläuft oder ein Benutzer erstellt wird. Wenn du nicht die Jellyfin-Login-Methode gewählt hast, stelle sicher, dass du deine E-Mail-Adresse angegeben hast."
"title": "Admin-Benachrichtigungen",
"description": "Wenn aktiviert, kannst du (pro Invite) wählen, eine Benachrichtigung zu erhalten, wenn ein Invite abläuft oder ein Benutzer erstellt wird. Wenn du nicht die Jellyfin-Login-Methode gewählt hast, stelle sicher, dass du deine E-Mail-Adresse angegeben hast, oder füge später eine andere Kontaktmöglichkeit hinzu."
},
"welcomeEmails": {
"title": "Willkommens-E-Mails",
"description": "Wenn aktiviert, wird eine E-Mail an neue Benutzer gesendet mit der Jellyfin-/Emby-URL und ihrem Benutzername."
"title": "Willkommens-Benachrichtigungen",
"description": "Wenn aktiviert, wird eine Benachrichtigung mit der Jellyfin-/Emby-URL und dem Benutzernamen an neue Benutzer gesendet."
},
"inviteEmails": {
"title": "Einladungs-E-Mails",
"description": "Wenn aktiviert, kannst du Invites direkt an die E-Mail-Adresse eines Benutzers schicken. Weil du möglicherweise einen Reverse-Proxy verwendest, musst du die URL angeben, von welcher auf Invites zugegriffen wird. Schreibe deine URL-Basis, und füge '/invite' hinzu."
"title": "Einladungs-Benachrichtigungen",
"description": "Wenn aktiviert, kannst du Invites direkt an die E-Mail-Adresse oder das Discord-/Matrix-Konto eines Benutzers schicken. Weil du möglicherweise einen Reverse-Proxy verwendest, musst du die URL angeben, von welcher auf Invites zugegriffen wird. Schreibe deine URL-Basis, und füge '/invite' hinzu."
},
"passwordResets": {
"title": "Passwortzurücksetzungen",
@ -104,7 +110,9 @@
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden.",
"resetLinks": "Einen Link statt einer PIN senden",
"resetLinksNotice": "Wenn die Ombi-Integration aktiviert ist, verwende dies, um zurückgesetzte Passwörter von Jellyfin mit Ombi zu synchronisieren.",
"resetLinksLanguage": "Standardsprache für den Link zum Zurücksetzen des Passworts"
"resetLinksLanguage": "Standardsprache für den Link zum Zurücksetzen des Passworts",
"setPassword": "Setze Password durch Link",
"setPasswordNotice": "Wenn aktiviert, muss der Benutzer sein Passwort nicht über eine PIN ändern. So wird ebenfalls die Passwortvalidierung erzwungen."
},
"passwordValidation": {
"title": "Passwortüberprüfung",
@ -133,5 +141,9 @@
"stable": "Stable",
"title": "Aktualisierungen",
"description": "Aktiviere, um informiert zu werden, wenn neue Aktualisierungen verfügbar sind. jfa-go wird {n} alle 30 Minuten überprüfen. Keine IP-Adressen oder personenbezogene Daten werden gesammelt."
},
"messages": {
"title": "Mitteilungen",
"description": "jfa-go kann Passwortrücksetzungen und verschiedene Benachrichtigungen per E-Mail, Discord, Telegram und/oder Matrix verschicken. E-Mail kannst du unten einrichten, die Anderen später in den Einstellungen. Anweisungen findest du im {n}. Falls nicht benötigt, kannst du diese Funktionen hier deaktivieren."
}
}

View File

@ -19,7 +19,8 @@
"errorInvalidUserPass": "Invalid username/password.",
"errorNotAdmin": "User is not allowed to manage server.",
"errorUserDisabled": "User may be disabled.",
"error404": "404, check the internal URL."
"error404": "404, check the internal URL.",
"errorConnectionRefused": "Connection refused."
},
"startPage": {
"welcome": "Welcome!",

View File

@ -58,7 +58,9 @@
"authorizeWithJellyfin": "Jellyfin/Emby authorisatie: Inloggegevens worden gedeeld met Jellyfin, meerdere gebruikers mogelijk.",
"authorizeManual": "Gebruikersnaam en wachtwoord: Stel handmatig een gebruikersnaam en wachtwoord in.",
"adminOnly": "Alleen beheerders (aanbevolen)",
"emailNotice": "Je e-mailadres kan gebruikt worden om meldingen te ontvangen."
"emailNotice": "Je e-mailadres kan gebruikt worden om meldingen te ontvangen.",
"allowAll": "Laat alle Jellyfin-gebruikers inloggen",
"allowAllDescription": "Afgeraden, je kunt beter individuele gebruikers toegang geven na de setup."
},
"jellyfinEmby": {
"title": "Jellyfin/Emby",

149
lang/setup/sl-si.json Normal file
View File

@ -0,0 +1,149 @@
{
"meta": {
"name": "Angleščina (ZDA)"
},
"strings": {
"pageTitle": "",
"next": "",
"back": "",
"optional": "",
"serverType": "",
"disabled": "",
"enabled": "",
"port": "",
"message": "",
"serverAddress": "",
"emailSubject": "",
"URL": "",
"apiKey": "",
"errorInvalidUserPass": "",
"errorNotAdmin": "",
"errorUserDisabled": "",
"error404": ""
},
"startPage": {
"welcome": "",
"pressStart": "",
"httpsNotice": "",
"start": ""
},
"endPage": {
"finished": "",
"restartMessage": "",
"refreshPage": ""
},
"language": {
"title": "",
"description": "",
"defaultAdminLang": "",
"defaultFormLang": "",
"defaultEmailLang": ""
},
"general": {
"title": "",
"listenAddress": "",
"urlBase": "",
"urlBaseNotice": "",
"lightTheme": "",
"darkTheme": "",
"useHTTPS": "",
"httpsPort": "",
"useHTTPSNotice": "",
"pathToCertificate": "",
"pathToKeyFile": ""
},
"updates": {
"title": "",
"description": "",
"updateChannel": "",
"stable": "",
"unstable": ""
},
"login": {
"title": "",
"description": "",
"authorizeWithJellyfin": "",
"authorizeManual": "",
"adminOnly": "",
"allowAll": "",
"allowAllDescription": "",
"emailNotice": ""
},
"jellyfinEmby": {
"title": "",
"description": "",
"embyNotice": "",
"internal": "",
"external": "",
"replaceJellyfin": "",
"replaceJellyfinNotice": "",
"addressExternalNotice": "",
"testConnection": ""
},
"ombi": {
"title": "",
"description": "",
"apiKeyNotice": ""
},
"messages": {
"title": "",
"description": ""
},
"email": {
"title": "",
"description": "",
"method": "",
"useEmailAsUsername": "",
"useEmailAsUsernameNotice": "",
"fromAddress": "",
"senderName": "",
"dateFormat": "",
"dateFormatNotice": "",
"encryption": "",
"mailgunApiURL": ""
},
"notifications": {
"title": "",
"description": ""
},
"welcomeEmails": {
"title": "",
"description": ""
},
"inviteEmails": {
"title": "",
"description": ""
},
"passwordResets": {
"title": "",
"description": "",
"pathToJellyfin": "",
"pathToJellyfinNotice": "",
"resetLinks": "",
"resetLinksNotice": "",
"resetLinksLanguage": "",
"setPassword": "",
"setPasswordNotice": ""
},
"passwordValidation": {
"title": "",
"description": "",
"length": "",
"uppercase": "",
"lowercase": "",
"numbers": "",
"special": ""
},
"helpMessages": {
"title": "",
"description": "",
"contactMessage": "",
"contactMessageNotice": "",
"helpMessage": "",
"helpMessageNotice": "",
"successMessage": "",
"successMessageNotice": "",
"emailMessage": "",
"emailMessageNotice": ""
}
}

16
lang/telegram/ar-AA.json Normal file
View File

@ -0,0 +1,16 @@
{
"meta": {
"name": "العربية (AR)"
},
"strings": {
"startMessage": "مرحبا!\nضع رقمك السري للتحقق من حسابك",
"discordStartMessage": "مرحبا!\nضع رقمك السري `/pin <PIN>` للتحقق من حسابك.",
"matrixStartMessage": "أهلا\nأدخل رقم التعريف الشخصي أدناه في صفحة الاشتراك في Jellyfin للتحقق من حسابك.",
"invalidPIN": "رقم التعريف الشخصي هذا غير صالح ، حاول مرة أخرى.",
"pinSuccess": "تم! يمكنك الآن العودة إلى صفحة التسجيل.",
"languageMessage": "ملاحظة: اطلع على اللغات المتاحة باستخدام {command} ، وقم بتعيين اللغة باستخدام {command} <language code>.",
"languageMessageDiscord": "ملاحظة: اضبط لغتك باستخدام / lang <language name>.",
"languageSet": "تم تعيين اللغة على {language}.",
"discordDMs": "يرجى التحقق من الرسائل المباشرة الخاصة بك للحصول على رد."
}
}

View File

@ -3,10 +3,14 @@
"name": "Deutsch (DE)"
},
"strings": {
"startMessage": "Hallo,\ngib deinen Jellyfin-PIN-Code ein, um dein Konto zu verifizieren.",
"matrixStartMessage": "Hallo,\ngib den untenstehenden PIN auf der Anmeldeseite von Jellyfin ein, um dein Konto zu verifizieren.",
"startMessage": "Hi!\nGib deinen Jellyfin-PIN-Code ein, um dein Konto zu verifizieren.",
"matrixStartMessage": "Hi!\nGib die untenstehende PIN auf der Anmeldeseite von Jellyfin ein, um dein Konto zu verifizieren.",
"invalidPIN": "Diese PIN war ungültig, versuche es erneut.",
"pinSuccess": "Erfolg! Du kannst nun zur Anmeldeseite zurückkehren.",
"languageMessage": "Hinweis: Zeige verfügbare Sprachen mit {command} an und stelle mit {command} <Sprachcode> die gewünschte ein."
"languageMessage": "Hinweis: Mit {command} kannst du alle verfügbaren Sprachen sehen und mit {command} <Sprachcode> die gewünschte Sprache einstellen.",
"discordStartMessage": "Hi!\n Gib \"/pin\" gefolgt von deiner PIN ein, um dein Konto zu verifizieren.",
"languageMessageDiscord": "Hinweis: Du kannst deine Sprache mit \"/lang <Sprache>\" ändern.",
"languageSet": "Sprache auf {language} geändert.",
"discordDMs": "Schaue in deinen DMs nach einer Antwort."
}
}

View File

@ -7,6 +7,10 @@
"matrixStartMessage": "Hallo\nVoer onderstaande pincode in op de Jellyfin aanmeldpagina om je account te verifiëren.",
"invalidPIN": "Die pincode was ongeldig, probeer het nogmaals.",
"pinSuccess": "Succes! Je kunt nu teruggaan naar de aanmeldpagina.",
"languageMessage": "Opmerking: Bekijk beschikbare talen met {command}, en stel de taal in met {command} <taalcode>."
"languageMessage": "Opmerking: Bekijk beschikbare talen met {command}, en stel de taal in met {command} <taalcode>.",
"discordStartMessage": "Hallo!\nVoer je pincode in met `/pin <PINCODE>` om je account te verifiëren.",
"languageMessageDiscord": "Opmerking: stel je taal in met /lang <taal>.",
"languageSet": "Taal ingesteld als {language}.",
"discordDMs": "Bekijk alsjeblieft je DMs voor een antwoord."
}
}

16
lang/telegram/sl-si.json Normal file
View File

@ -0,0 +1,16 @@
{
"meta": {
"name": "Angleščina (ZDA)"
},
"strings": {
"startMessage": "Pozdravljeni!\nVnesite svoj Jellyfin PIN da potrdite svoj račun.",
"discordStartMessage": "Pozdravljeni!\n Vnesite svoj PIN v formatu `/pin <PIN>` da potrdite svoj račun.",
"matrixStartMessage": "Pozdravljeni\nVnesite spodnji PIN na Jellyfin stran za registracijo da potrdite svoj račun.",
"invalidPIN": "Ta PIN je neveljaven, prosimo poskusite ponovno.",
"pinSuccess": "Uspeh! Sedaj se lahko vrnete na stran za registracijo.",
"languageMessage": "Opomin: Druge jezike lahko vidite s pomočjo {command}, in ga nastavite s pomočjo {command} <language code>.",
"languageMessageDiscord": "Opomin: nastavite jezik z ukazom /lang <language name>.",
"languageSet": "Jezik nastavljen na {language}.",
"discordDMs": "Prosimo preverite svoja zasebna sporočila za odziv."
}
}

16
package-lock.json generated
View File

@ -20,7 +20,6 @@
"jsdom": "^19.0.0",
"lodash": "^4.17.21",
"mjml": "^4.12.0",
"nightwind": "github:yonson2/nightwind",
"perl-regex": "^1.0.4",
"postcss": "^8.4.5",
"remixicon": "^2.5.0",
@ -4067,15 +4066,6 @@
"node": ">= 0.6"
}
},
"node_modules/nightwind": {
"version": "1.1.12",
"resolved": "git+ssh://git@github.com/yonson2/nightwind.git#755e4b34e0cd782291822b88ad969c69404900da",
"integrity": "sha512-eibdkEdOFtBj/1iTk3W0CP9+GXwW1OXcoLBm+Pf7Sq5JLRmailuhaxGrKlNjZH9Wn8yYPO+LuYHg3NMG27Gjuw==",
"license": "MIT",
"peerDependencies": {
"tailwindcss": ">= 2.0.0"
}
},
"node_modules/no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
@ -10163,12 +10153,6 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"dev": true
},
"nightwind": {
"version": "git+ssh://git@github.com/yonson2/nightwind.git#755e4b34e0cd782291822b88ad969c69404900da",
"integrity": "sha512-eibdkEdOFtBj/1iTk3W0CP9+GXwW1OXcoLBm+Pf7Sq5JLRmailuhaxGrKlNjZH9Wn8yYPO+LuYHg3NMG27Gjuw==",
"from": "nightwind@github:yonson2/nightwind",
"requires": {}
},
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",

View File

@ -28,7 +28,6 @@
"jsdom": "^19.0.0",
"lodash": "^4.17.21",
"mjml": "^4.12.0",
"nightwind": "github:yonson2/nightwind",
"perl-regex": "^1.0.4",
"postcss": "^8.4.5",
"remixicon": "^2.5.0",

View File

@ -68,6 +68,9 @@ func (app *appContext) TestJF(gc *gin.Context) {
if !(status == 200 || status == 204) || err != nil {
msg := ""
switch status {
case 0:
msg = "errorConnectionRefused"
status = 500
case 401:
msg = "errorInvalidUserPass"
case 403: