mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
messages: custom content described in customcontent.go, message tests
customcontent.go constains a structure with all the custom content, methods for getting display names, subjects, etc., and a list of variables, conditionals, and placeholder values. Tests for constructX methods included in email_test.go, and all jfa-go tests can be run with make INTERNAL=off test.
This commit is contained in:
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: configuration email typescript swagger copy compile compress inline-css variants-html install clean npm config-description config-default precompile
|
.PHONY: configuration email typescript swagger copy compile compress inline-css variants-html install clean npm config-description config-default precompile test
|
||||||
.DEFAULT_GOAL := all
|
.DEFAULT_GOAL := all
|
||||||
|
|
||||||
GOESBUILD ?= off
|
GOESBUILD ?= off
|
||||||
@@ -216,13 +216,16 @@ ifeq ($(INTERNAL), on)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
GO_SRC = $(shell find ./ -name "*.go")
|
GO_SRC = $(shell find ./ -name "*.go")
|
||||||
GO_TARGET = build/jfa-go
|
GO_TARGET = build/jfa-go
|
||||||
$(GO_TARGET): $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
$(GO_TARGET): $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
||||||
$(info Downloading deps)
|
$(info Downloading deps)
|
||||||
$(GOBINARY) mod download
|
$(GOBINARY) mod download
|
||||||
$(info Building)
|
$(info Building)
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
|
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
|
||||||
|
|
||||||
|
test: $(BUILDDEPS) $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
||||||
|
$(GOBINARY) test -ldflags="$(LDFLAGS)" $(TAGS) -p 1
|
||||||
|
|
||||||
all: $(BUILDDEPS) $(GO_TARGET)
|
all: $(BUILDDEPS) $(GO_TARGET)
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup
|
|||||||
wait.Add(1)
|
wait.Add(1)
|
||||||
go func(addr string) {
|
go func(addr string) {
|
||||||
defer wait.Done()
|
defer wait.Done()
|
||||||
msg, err := app.email.constructExpiry(data.Code, data, app, false)
|
msg, err := app.email.constructExpiry(data, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructExpiryAdmin, data.Code, err)
|
app.err.Printf(lm.FailedConstructExpiryAdmin, data.Code, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -218,7 +218,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
invite.SendTo = req.SendTo
|
invite.SendTo = req.SendTo
|
||||||
}
|
}
|
||||||
if addressValid {
|
if addressValid {
|
||||||
msg, err := app.email.constructInvite(invite.Code, invite, app, false)
|
msg, err := app.email.constructInvite(invite, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Slight misuse of the template
|
// Slight misuse of the template
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
|
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
|
||||||
@@ -343,7 +343,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
// These used to be stored formatted instead of as a unix timestamp.
|
// These used to be stored formatted instead of as a unix timestamp.
|
||||||
unix, err := strconv.ParseInt(pair[1], 10, 64)
|
unix, err := strconv.ParseInt(pair[1], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
date, err := timefmt.Parse(pair[1], datePattern+" "+timePattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedParseTime, err)
|
app.err.Printf(lm.FailedParseTime, err)
|
||||||
}
|
}
|
||||||
|
|||||||
235
api-messages.go
235
api-messages.go
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -23,26 +22,16 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
|
|||||||
if _, ok := app.storage.lang.Email[lang]; !ok {
|
if _, ok := app.storage.lang.Email[lang]; !ok {
|
||||||
lang = app.storage.lang.chosenEmailLang
|
lang = app.storage.lang.chosenEmailLang
|
||||||
}
|
}
|
||||||
adminLang := lang
|
list := emailListDTO{}
|
||||||
if _, ok := app.storage.lang.Admin[lang]; !ok {
|
for _, cc := range customContent {
|
||||||
adminLang = app.storage.lang.chosenAdminLang
|
if cc.ContentType == CustomTemplate {
|
||||||
}
|
continue
|
||||||
list := emailListDTO{
|
}
|
||||||
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.MustGetCustomContentKey("UserCreated").Enabled},
|
ccDescription := emailListEl{Name: cc.DisplayName(&app.storage.lang, lang), Enabled: app.storage.MustGetCustomContentKey(cc.Name).Enabled}
|
||||||
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.MustGetCustomContentKey("InviteExpiry").Enabled},
|
if cc.Description != nil {
|
||||||
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.MustGetCustomContentKey("PasswordReset").Enabled},
|
ccDescription.Description = cc.Description(&app.storage.lang, lang)
|
||||||
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled},
|
}
|
||||||
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled},
|
list[cc.Name] = ccDescription
|
||||||
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled},
|
|
||||||
"UserExpiryAdjusted": {Name: app.storage.lang.Email[lang].UserExpiryAdjusted["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpiryAdjusted").Enabled},
|
|
||||||
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled},
|
|
||||||
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled},
|
|
||||||
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled},
|
|
||||||
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled},
|
|
||||||
"ExpiryReminder": {Name: app.storage.lang.Email[lang].ExpiryReminder["name"], Enabled: app.storage.MustGetCustomContentKey("ExpiryReminder").Enabled},
|
|
||||||
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("UserLogin").Enabled},
|
|
||||||
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("UserPage").Enabled},
|
|
||||||
"PostSignupCard": {Name: app.storage.lang.Admin[adminLang].Strings["postSignupCard"], Enabled: app.storage.MustGetCustomContentKey("PostSignupCard").Enabled, Description: app.storage.lang.Admin[adminLang].Strings["postSignupCardDescription"]},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := gc.Query("filter")
|
filter := gc.Query("filter")
|
||||||
@@ -74,11 +63,12 @@ func (app *appContext) SetCustomMessage(gc *gin.Context) {
|
|||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message, ok := app.storage.GetCustomContentKey(id)
|
_, ok := customContent[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
message, ok := app.storage.GetCustomContentKey(id)
|
||||||
message.Content = req.Content
|
message.Content = req.Content
|
||||||
message.Enabled = true
|
message.Enabled = true
|
||||||
app.storage.SetCustomContentKey(id, message)
|
app.storage.SetCustomContentKey(id, message)
|
||||||
@@ -124,151 +114,92 @@ func (app *appContext) SetCustomMessageState(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||||
lang := app.storage.lang.chosenEmailLang
|
|
||||||
id := gc.Param("id")
|
id := gc.Param("id")
|
||||||
var content string
|
|
||||||
var err error
|
var err error
|
||||||
var msg *Message
|
contentInfo, ok := customContent[id]
|
||||||
var variables []string
|
// FIXME: Add announcement to customContent
|
||||||
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")
|
|
||||||
customMessage, ok := app.storage.GetCustomContentKey(id)
|
|
||||||
if !ok && id != "Announcement" {
|
if !ok && id != "Announcement" {
|
||||||
app.err.Printf(lm.FailedGetCustomMessage, id)
|
app.err.Printf(lm.FailedGetCustomMessage, id)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if id == "WelcomeEmail" {
|
|
||||||
conditionals = []string{"{yourAccountWillExpire}"}
|
content, ok := app.storage.GetCustomContentKey(id)
|
||||||
customMessage.Conditionals = conditionals
|
|
||||||
} else if id == "UserPage" {
|
if contentInfo.Variables == nil {
|
||||||
variables = []string{"{username}"}
|
contentInfo.Variables = []string{}
|
||||||
customMessage.Variables = variables
|
}
|
||||||
} else if id == "UserLogin" {
|
if contentInfo.Conditionals == nil {
|
||||||
variables = []string{}
|
contentInfo.Conditionals = []string{}
|
||||||
customMessage.Variables = variables
|
}
|
||||||
} else if id == "PostSignupCard" {
|
if contentInfo.Placeholders == nil {
|
||||||
variables = []string{"{username}", "{myAccountURL}"}
|
contentInfo.Placeholders = map[string]any{}
|
||||||
customMessage.Variables = variables
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content = customMessage.Content
|
// Generate content from real email, if the user hasn't already customised this message.
|
||||||
noContent := content == ""
|
if content.Content == "" {
|
||||||
if !noContent {
|
var msg *Message
|
||||||
variables = customMessage.Variables
|
switch id {
|
||||||
|
// FIXME: Add announcement to customContent
|
||||||
|
case "UserCreated":
|
||||||
|
msg, err = app.email.constructCreated("", "", time.Time{}, Invite{}, true)
|
||||||
|
case "InviteExpiry":
|
||||||
|
msg, err = app.email.constructExpiry(Invite{}, true)
|
||||||
|
case "PasswordReset":
|
||||||
|
msg, err = app.email.constructReset(PasswordReset{}, true)
|
||||||
|
case "UserDeleted":
|
||||||
|
msg, err = app.email.constructDeleted("", true)
|
||||||
|
case "UserDisabled":
|
||||||
|
msg, err = app.email.constructDisabled("", true)
|
||||||
|
case "UserEnabled":
|
||||||
|
msg, err = app.email.constructEnabled("", true)
|
||||||
|
case "UserExpiryAdjusted":
|
||||||
|
msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", true)
|
||||||
|
case "ExpiryReminder":
|
||||||
|
msg, err = app.email.constructExpiryReminder("", time.Now().AddDate(0, 0, 3), true)
|
||||||
|
case "InviteEmail":
|
||||||
|
msg, err = app.email.constructInvite(Invite{Code: ""}, true)
|
||||||
|
case "WelcomeEmail":
|
||||||
|
msg, err = app.email.constructWelcome("", time.Time{}, true)
|
||||||
|
case "EmailConfirmation":
|
||||||
|
msg, err = app.email.constructConfirmation("", "", "", true)
|
||||||
|
case "UserExpired":
|
||||||
|
msg, err = app.email.constructUserExpired(true)
|
||||||
|
case "Announcement":
|
||||||
|
case "UserPage":
|
||||||
|
case "UserLogin":
|
||||||
|
case "PostSignupCard":
|
||||||
|
// These don't have any example content
|
||||||
|
msg = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg != nil {
|
||||||
|
content.Content = msg.Text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 "UserExpiryAdjusted":
|
|
||||||
if noContent {
|
|
||||||
msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", app, true)
|
|
||||||
}
|
|
||||||
values = app.email.expiryAdjustedValues(username, time.Time{}, app.storage.lang.Email[lang].Strings.get("reason"), app, false, true)
|
|
||||||
case "ExpiryReminder":
|
|
||||||
if noContent {
|
|
||||||
msg, err = app.email.constructExpiryReminder("", time.Now().AddDate(0, 0, 3), app, true)
|
|
||||||
}
|
|
||||||
values = app.email.expiryReminderValues(username, time.Now().AddDate(0, 0, 3), app, false, true)
|
|
||||||
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)
|
|
||||||
case "UserLogin", "UserPage", "PostSignupCard":
|
|
||||||
values = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" && id != "PostSignupCard" {
|
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customMessage.Variables = variables
|
|
||||||
}
|
|
||||||
if variables == nil {
|
|
||||||
variables = []string{}
|
|
||||||
}
|
|
||||||
app.storage.SetCustomContentKey(id, customMessage)
|
|
||||||
var mail *Message
|
var mail *Message
|
||||||
if id != "UserLogin" && id != "UserPage" && id != "PostSignupCard" {
|
if contentInfo.ContentType == CustomMessage {
|
||||||
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
mail = &Message{}
|
||||||
|
err = app.email.construct(EmptyCustomContent, CustomContent{
|
||||||
|
Name: EmptyCustomContent.Name,
|
||||||
|
Enabled: true,
|
||||||
|
Content: "<div class=\"preview-content\"></div>",
|
||||||
|
}, map[string]any{}, mail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if id == "PostSignupCard" {
|
} else if id == "PostSignupCard" {
|
||||||
// Jankiness follows.
|
// Specific workaround for the currently-unique "Post signup card".
|
||||||
// Source content from "Success Message" setting.
|
// Source content from "Success Message" setting.
|
||||||
if noContent {
|
if content.Content == "" {
|
||||||
content = "# " + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("successHeader") + "\n" + app.config.Section("ui").Key("success_message").String()
|
content.Content = "# " + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("successHeader") + "\n" + app.config.Section("ui").Key("success_message").String()
|
||||||
if app.config.Section("user_page").Key("enabled").MustBool(false) {
|
if app.config.Section("user_page").Key("enabled").MustBool(false) {
|
||||||
content += "\n\n<br>\n" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.template("userPageSuccessMessage", tmpl{
|
content.Content += "\n\n<br>\n" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.template("userPageSuccessMessage", tmpl{
|
||||||
"myAccount": "[" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("myAccount") + "]({myAccountURL})",
|
"myAccount": "[" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("myAccount") + "]({myAccountURL})",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -277,13 +208,15 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
HTML: "<div class=\"card ~neutral dark:~d_neutral @low\"><div class=\"preview-content\"></div><br><button class=\"button ~urge dark:~d_urge @low full-width center supra submit\">" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("continue") + "</a></div>",
|
HTML: "<div class=\"card ~neutral dark:~d_neutral @low\"><div class=\"preview-content\"></div><br><button class=\"button ~urge dark:~d_urge @low full-width center supra submit\">" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("continue") + "</a></div>",
|
||||||
}
|
}
|
||||||
mail.Markdown = mail.HTML
|
mail.Markdown = mail.HTML
|
||||||
} else {
|
} else if contentInfo.ContentType == CustomCard {
|
||||||
mail = &Message{
|
mail = &Message{
|
||||||
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
|
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
|
||||||
}
|
}
|
||||||
mail.Markdown = mail.HTML
|
mail.Markdown = mail.HTML
|
||||||
|
} else {
|
||||||
|
app.err.Printf("unknown custom content type %d", contentInfo.ContentType)
|
||||||
}
|
}
|
||||||
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
|
gc.JSON(200, customEmailDTO{Content: content.Content, Variables: contentInfo.Variables, Conditionals: contentInfo.Conditionals, Values: contentInfo.Placeholders, HTML: mail.HTML, Plaintext: mail.Text})
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Returns a new Telegram verification PIN, and the bot username.
|
// @Summary Returns a new Telegram verification PIN, and the bot username.
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
||||||
respond(401, "confirmEmail", gc)
|
respond(401, "confirmEmail", gc)
|
||||||
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
msg, err := app.email.constructConfirmation("", name, key, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructConfirmationEmail, id, err)
|
app.err.Printf(lm.FailedConstructConfirmationEmail, id, err)
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||||
@@ -643,7 +643,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
Username: pwr.Username,
|
Username: pwr.Username,
|
||||||
Expiry: pwr.Expiry,
|
Expiry: pwr.Expiry,
|
||||||
Internal: true,
|
Internal: true,
|
||||||
}, app, false,
|
}, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
||||||
|
|||||||
30
api-users.go
30
api-users.go
@@ -189,7 +189,7 @@ func (app *appContext) NewUserFromInvite(gc *gin.Context) {
|
|||||||
|
|
||||||
app.debug.Printf(lm.EmailConfirmationRequired, req.Username)
|
app.debug.Printf(lm.EmailConfirmationRequired, req.Username)
|
||||||
respond(401, "confirmEmail", gc)
|
respond(401, "confirmEmail", gc)
|
||||||
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false)
|
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructConfirmationEmail, req.Code, err)
|
app.err.Printf(lm.FailedConstructConfirmationEmail, req.Code, err)
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||||
@@ -262,7 +262,7 @@ func (app *appContext) PostNewUserFromInvite(nu NewUserData, req ConfirmationKey
|
|||||||
}
|
}
|
||||||
app.contactMethods[i].DeleteVerifiedToken(c.PIN)
|
app.contactMethods[i].DeleteVerifiedToken(c.PIN)
|
||||||
c.User.SetJellyfin(nu.User.ID)
|
c.User.SetJellyfin(nu.User.ID)
|
||||||
c.User.Store(&(app.storage))
|
c.User.Store(app.storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ func (app *appContext) PostNewUserFromInvite(nu NewUserData, req ConfirmationKey
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go func(addr string) {
|
go func(addr string) {
|
||||||
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
|
msg, err := app.email.constructCreated(req.Username, req.Email, time.Now(), invite, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructCreationAdmin, req.Code, err)
|
app.err.Printf(lm.FailedConstructCreationAdmin, req.Code, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -384,9 +384,9 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
|
|||||||
var err error
|
var err error
|
||||||
if sendMail {
|
if sendMail {
|
||||||
if req.Enabled {
|
if req.Enabled {
|
||||||
msg, err = app.email.constructEnabled(req.Reason, app, false)
|
msg, err = app.email.constructEnabled(req.Reason, false)
|
||||||
} else {
|
} else {
|
||||||
msg, err = app.email.constructDisabled(req.Reason, app, false)
|
msg, err = app.email.constructDisabled(req.Reason, false)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err)
|
app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err)
|
||||||
@@ -452,7 +452,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
|||||||
var msg *Message
|
var msg *Message
|
||||||
var err error
|
var err error
|
||||||
if sendMail {
|
if sendMail {
|
||||||
msg, err = app.email.constructDeleted(req.Reason, app, false)
|
msg, err = app.email.constructDeleted(req.Reason, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructDeletionMessage, "?", err)
|
app.err.Printf(lm.FailedConstructDeletionMessage, "?", err)
|
||||||
sendMail = false
|
sendMail = false
|
||||||
@@ -541,7 +541,7 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false)
|
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructExpiryAdjustmentMessage, uid, err)
|
app.err.Printf(lm.FailedConstructExpiryAdjustmentMessage, uid, err)
|
||||||
return
|
return
|
||||||
@@ -677,7 +677,11 @@ func (app *appContext) Announce(gc *gin.Context) {
|
|||||||
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msg, err := app.email.constructTemplate(req.Subject, req.Message, app, user.Name)
|
msg := &Message{}
|
||||||
|
err = app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
|
||||||
|
Enabled: true,
|
||||||
|
Content: req.Message,
|
||||||
|
}, map[string]any{"username": user.Name}, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err)
|
app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
@@ -690,7 +694,11 @@ func (app *appContext) Announce(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
|
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
|
||||||
} else {
|
} else {
|
||||||
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
|
msg := &Message{}
|
||||||
|
err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
|
||||||
|
Enabled: true,
|
||||||
|
Content: req.Message,
|
||||||
|
}, map[string]any{"username": ""}, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err)
|
app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
@@ -810,7 +818,7 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
|
|||||||
app.internalPWRs[pwr.PIN] = pwr
|
app.internalPWRs[pwr.PIN] = pwr
|
||||||
sendAddress := app.getAddressOrName(id)
|
sendAddress := app.getAddressOrName(id)
|
||||||
if sendAddress == "" || len(req.Users) == 1 {
|
if sendAddress == "" || len(req.Users) == 1 {
|
||||||
resp.Link, err = app.GenResetLink(pwr.PIN)
|
resp.Link, err = GenResetLink(pwr.PIN)
|
||||||
linkCount++
|
linkCount++
|
||||||
if sendAddress == "" {
|
if sendAddress == "" {
|
||||||
resp.Manual = true
|
resp.Manual = true
|
||||||
@@ -823,7 +831,7 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
|
|||||||
Username: pwr.Username,
|
Username: pwr.Username,
|
||||||
Expiry: pwr.Expiry,
|
Expiry: pwr.Expiry,
|
||||||
Internal: true,
|
Internal: true,
|
||||||
}, app, false,
|
}, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructPWRMessage, id, err)
|
app.err.Printf(lm.FailedConstructPWRMessage, id, err)
|
||||||
|
|||||||
21
api.go
21
api.go
@@ -36,23 +36,14 @@ func respondBool(code int, val bool, gc *gin.Context) {
|
|||||||
gc.Abort()
|
gc.Abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) loadStrftime() {
|
func prettyTime(dt time.Time) (date, time string) {
|
||||||
app.datePattern = app.config.Section("messages").Key("date_format").String()
|
date = timefmt.Format(dt, datePattern)
|
||||||
app.timePattern = `%H:%M`
|
time = timefmt.Format(dt, timePattern)
|
||||||
if val, _ := app.config.Section("messages").Key("use_24h").Bool(); !val {
|
|
||||||
app.timePattern = `%I:%M %p`
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) prettyTime(dt time.Time) (date, time string) {
|
func formatDatetime(dt time.Time) string {
|
||||||
date = timefmt.Format(dt, app.datePattern)
|
d, t := prettyTime(dt)
|
||||||
time = timefmt.Format(dt, app.timePattern)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) formatDatetime(dt time.Time) string {
|
|
||||||
d, t := app.prettyTime(dt)
|
|
||||||
return d + " " + t
|
return d + " " + t
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +301,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
||||||
app.Restart()
|
app.Restart()
|
||||||
}
|
}
|
||||||
app.loadConfig()
|
app.ReloadConfig()
|
||||||
// Patch new settings for next GetConfig
|
// Patch new settings for next GetConfig
|
||||||
app.PatchConfigBase()
|
app.PatchConfigBase()
|
||||||
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
BACKUP_PREFIX = "jfa-go-db"
|
BACKUP_PREFIX = "jfa-go-db"
|
||||||
|
BACKUP_PREFIX_OLD = "jfa-go-db-"
|
||||||
BACKUP_COMMIT_PREFIX = "-c-"
|
BACKUP_COMMIT_PREFIX = "-c-"
|
||||||
BACKUP_DATE_PREFIX = "-d-"
|
BACKUP_DATE_PREFIX = "-d-"
|
||||||
BACKUP_UPLOAD_PREFIX = "upload-"
|
BACKUP_UPLOAD_PREFIX = "upload-"
|
||||||
@@ -33,7 +34,7 @@ func (b Backup) Equals(a Backup) bool {
|
|||||||
return a.Date.Equal(b.Date) && a.Commit == b.Commit && a.Upload == b.Upload
|
return a.Date.Equal(b.Date) && a.Commit == b.Commit && a.Upload == b.Upload
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre 21/03/25 format: "{BACKUP_PREFIX}{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-2006-01-02T15-04-05.bak"
|
// Pre 21/03/25 format: "{BACKUP_PREFIX_OLD}{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-2006-01-02T15-04-05.bak"
|
||||||
// Post 21/03/25 format: "{BACKUP_PREFIX}-c-{commit}-d-{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-c-0b92060-d-2006-01-02T15-04-05.bak"
|
// Post 21/03/25 format: "{BACKUP_PREFIX}-c-{commit}-d-{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-c-0b92060-d-2006-01-02T15-04-05.bak"
|
||||||
|
|
||||||
func (b Backup) String() string {
|
func (b Backup) String() string {
|
||||||
@@ -274,8 +275,10 @@ func (app *appContext) loadPendingBackup() {
|
|||||||
}
|
}
|
||||||
app.info.Printf(lm.MoveOldDB, oldPath)
|
app.info.Printf(lm.MoveOldDB, oldPath)
|
||||||
|
|
||||||
app.ConnectDB()
|
if err := app.storage.Connect(app.config); err != nil {
|
||||||
defer app.storage.db.Close()
|
app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err)
|
||||||
|
}
|
||||||
|
defer app.storage.Close()
|
||||||
|
|
||||||
f, err := os.Open(LOADBAK)
|
f, err := os.Open(LOADBAK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ func testBackupParse(f string, a Backup, t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackupParserOld(t *testing.T) {
|
func TestBackupParserOld(t *testing.T) {
|
||||||
Q1 := BACKUP_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
Q1 := BACKUP_PREFIX_OLD + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
||||||
A1 := Backup{}
|
A1 := Backup{}
|
||||||
A1.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
A1.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
||||||
testBackupParse(Q1, A1, t)
|
testBackupParse(Q1, A1, t)
|
||||||
}
|
}
|
||||||
func TestBackupParserOldUpload(t *testing.T) {
|
func TestBackupParserOldUpload(t *testing.T) {
|
||||||
Q2 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
Q2 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX_OLD + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
||||||
A2 := Backup{
|
A2 := Backup{
|
||||||
Upload: true,
|
Upload: true,
|
||||||
}
|
}
|
||||||
|
|||||||
370
config.go
370
config.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -18,6 +19,12 @@ import (
|
|||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
*ini.File
|
||||||
|
proxyTransport *http.Transport
|
||||||
|
proxyConfig *easyproxy.ProxyConfig
|
||||||
|
}
|
||||||
|
|
||||||
var emailEnabled = false
|
var emailEnabled = false
|
||||||
var messagesEnabled = false
|
var messagesEnabled = false
|
||||||
var telegramEnabled = false
|
var telegramEnabled = false
|
||||||
@@ -28,8 +35,8 @@ var matrixEnabled = false
|
|||||||
// IMPORTANT: When linking straight to a page, rather than appending further to the URL (like accessing an API route), append a /.
|
// IMPORTANT: When linking straight to a page, rather than appending further to the URL (like accessing an API route), append a /.
|
||||||
var PAGES = PagePaths{}
|
var PAGES = PagePaths{}
|
||||||
|
|
||||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
func (config *Config) GetPath(sect, key string) (fs.FS, string) {
|
||||||
val := app.config.Section(sect).Key(key).MustString("")
|
val := config.Section(sect).Key(key).MustString("")
|
||||||
if strings.HasPrefix(val, "jfa-go:") {
|
if strings.HasPrefix(val, "jfa-go:") {
|
||||||
return localFS, strings.TrimPrefix(val, "jfa-go:")
|
return localFS, strings.TrimPrefix(val, "jfa-go:")
|
||||||
}
|
}
|
||||||
@@ -37,15 +44,15 @@ func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
|||||||
return os.DirFS(dir), file
|
return os.DirFS(dir), file
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) MustSetValue(section, key, val string) {
|
func (config *Config) MustSetValue(section, key, val string) {
|
||||||
app.config.Section(section).Key(key).SetValue(app.config.Section(section).Key(key).MustString(val))
|
config.Section(section).Key(key).SetValue(config.Section(section).Key(key).MustString(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) MustSetURLPath(section, key, val string) {
|
func (config *Config) MustSetURLPath(section, key, val string) {
|
||||||
if !strings.HasPrefix(val, "/") && val != "" {
|
if !strings.HasPrefix(val, "/") && val != "" {
|
||||||
val = "/" + val
|
val = "/" + val
|
||||||
}
|
}
|
||||||
app.MustSetValue(section, key, val)
|
config.MustSetValue(section, key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FixFullURL(v string) string {
|
func FixFullURL(v string) string {
|
||||||
@@ -69,26 +76,26 @@ func FormatSubpath(path string, removeSingleSlash bool) string {
|
|||||||
return strings.TrimSuffix(path, "/")
|
return strings.TrimSuffix(path, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) MustCorrectURL(section, key, value string) {
|
func (config *Config) MustCorrectURL(section, key, value string) {
|
||||||
v := app.config.Section(section).Key(key).String()
|
v := config.Section(section).Key(key).String()
|
||||||
if v == "" {
|
if v == "" {
|
||||||
v = value
|
v = value
|
||||||
}
|
}
|
||||||
v = FixFullURL(v)
|
v = FixFullURL(v)
|
||||||
app.config.Section(section).Key(key).SetValue(v)
|
config.Section(section).Key(key).SetValue(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalDomain returns the Host for the request, using the fixed app.externalDomain value unless app.UseProxyHost is true.
|
// ExternalDomain returns the Host for the request, using the fixed externalDomain value unless UseProxyHost is true.
|
||||||
func (app *appContext) ExternalDomain(gc *gin.Context) string {
|
func ExternalDomain(gc *gin.Context) string {
|
||||||
if !app.UseProxyHost || gc.Request.Host == "" {
|
if !UseProxyHost || gc.Request.Host == "" {
|
||||||
return app.externalDomain
|
return externalDomain
|
||||||
}
|
}
|
||||||
return gc.Request.Host
|
return gc.Request.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalDomainNoPort attempts to return app.ExternalDomain() with the port removed. If the internally-used method fails, it is assumed the domain has no port anyway.
|
// ExternalDomainNoPort attempts to return ExternalDomain() with the port removed. If the internally-used method fails, it is assumed the domain has no port anyway.
|
||||||
func (app *appContext) ExternalDomainNoPort(gc *gin.Context) string {
|
func (app *appContext) ExternalDomainNoPort(gc *gin.Context) string {
|
||||||
domain := app.ExternalDomain(gc)
|
domain := ExternalDomain(gc)
|
||||||
host, _, err := net.SplitHostPort(domain)
|
host, _, err := net.SplitHostPort(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain
|
return domain
|
||||||
@@ -96,11 +103,11 @@ func (app *appContext) ExternalDomainNoPort(gc *gin.Context) string {
|
|||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalURI returns the External URI of jfa-go's root directory (by default, where the admin page is), using the fixed app.externalURI value unless app.UseProxyHost is true and gc is not nil.
|
// ExternalURI returns the External URI of jfa-go's root directory (by default, where the admin page is), using the fixed externalURI value unless UseProxyHost is true and gc is not nil.
|
||||||
// When nil is passed, app.externalURI is returned.
|
// When nil is passed, externalURI is returned.
|
||||||
func (app *appContext) ExternalURI(gc *gin.Context) string {
|
func ExternalURI(gc *gin.Context) string {
|
||||||
if gc == nil {
|
if gc == nil {
|
||||||
return app.externalURI
|
return externalURI
|
||||||
}
|
}
|
||||||
|
|
||||||
var proto string
|
var proto string
|
||||||
@@ -111,10 +118,10 @@ func (app *appContext) ExternalURI(gc *gin.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// app.debug.Printf("Request: %+v\n", gc.Request)
|
// app.debug.Printf("Request: %+v\n", gc.Request)
|
||||||
if app.UseProxyHost && gc.Request.Host != "" {
|
if UseProxyHost && gc.Request.Host != "" {
|
||||||
return proto + gc.Request.Host + PAGES.Base
|
return proto + gc.Request.Host + PAGES.Base
|
||||||
}
|
}
|
||||||
return app.externalURI
|
return externalURI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) EvaluateRelativePath(gc *gin.Context, path string) string {
|
func (app *appContext) EvaluateRelativePath(gc *gin.Context, path string) string {
|
||||||
@@ -129,177 +136,192 @@ func (app *appContext) EvaluateRelativePath(gc *gin.Context, path string) string
|
|||||||
proto = "http://"
|
proto = "http://"
|
||||||
}
|
}
|
||||||
|
|
||||||
return proto + app.ExternalDomain(gc) + path
|
return proto + ExternalDomain(gc) + path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) loadConfig() error {
|
// NewConfig reads and patches a config file for use. Passed loggers are used only once. Some dependencies can be reloaded after this is called with ReloadDependents(app).
|
||||||
|
func NewConfig(configPathOrContents any, dataPath string, logs LoggerSet) (*Config, error) {
|
||||||
var err error
|
var err error
|
||||||
app.config, err = ini.ShadowLoad(app.configPath)
|
config := &Config{}
|
||||||
|
config.File, err = ini.ShadowLoad(configPathOrContents)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLs
|
// URLs
|
||||||
app.MustSetURLPath("ui", "url_base", "")
|
config.MustSetURLPath("ui", "url_base", "")
|
||||||
app.MustSetURLPath("url_paths", "admin", "")
|
config.MustSetURLPath("url_paths", "admin", "")
|
||||||
app.MustSetURLPath("url_paths", "user_page", "/my/account")
|
config.MustSetURLPath("url_paths", "user_page", "/my/account")
|
||||||
app.MustSetURLPath("url_paths", "form", "/invite")
|
config.MustSetURLPath("url_paths", "form", "/invite")
|
||||||
PAGES.Base = FormatSubpath(app.config.Section("ui").Key("url_base").String(), true)
|
PAGES.Base = FormatSubpath(config.Section("ui").Key("url_base").String(), true)
|
||||||
PAGES.Admin = FormatSubpath(app.config.Section("url_paths").Key("admin").String(), true)
|
PAGES.Admin = FormatSubpath(config.Section("url_paths").Key("admin").String(), true)
|
||||||
PAGES.MyAccount = FormatSubpath(app.config.Section("url_paths").Key("user_page").String(), true)
|
PAGES.MyAccount = FormatSubpath(config.Section("url_paths").Key("user_page").String(), true)
|
||||||
PAGES.Form = FormatSubpath(app.config.Section("url_paths").Key("form").String(), true)
|
PAGES.Form = FormatSubpath(config.Section("url_paths").Key("form").String(), true)
|
||||||
if !(app.config.Section("user_page").Key("enabled").MustBool(true)) {
|
if !(config.Section("user_page").Key("enabled").MustBool(true)) {
|
||||||
PAGES.MyAccount = "disabled"
|
PAGES.MyAccount = "disabled"
|
||||||
}
|
}
|
||||||
if PAGES.Base == PAGES.Form || PAGES.Base == "/accounts" || PAGES.Base == "/settings" || PAGES.Base == "/activity" {
|
if PAGES.Base == PAGES.Form || PAGES.Base == "/accounts" || PAGES.Base == "/settings" || PAGES.Base == "/activity" {
|
||||||
app.err.Printf(lm.BadURLBase, PAGES.Base)
|
logs.err.Printf(lm.BadURLBase, PAGES.Base)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.SubpathBlockMessage, PAGES.Base, PAGES.Admin, PAGES.MyAccount, PAGES.Form)
|
logs.info.Printf(lm.SubpathBlockMessage, PAGES.Base, PAGES.Admin, PAGES.MyAccount, PAGES.Form)
|
||||||
|
|
||||||
app.MustCorrectURL("jellyfin", "server", "")
|
config.MustCorrectURL("jellyfin", "server", "")
|
||||||
app.MustCorrectURL("jellyfin", "public_server", app.config.Section("jellyfin").Key("server").String())
|
config.MustCorrectURL("jellyfin", "public_server", config.Section("jellyfin").Key("server").String())
|
||||||
app.MustCorrectURL("ui", "redirect_url", app.config.Section("jellyfin").Key("public_server").String())
|
config.MustCorrectURL("ui", "redirect_url", config.Section("jellyfin").Key("public_server").String())
|
||||||
|
|
||||||
for _, key := range app.config.Section("files").Keys() {
|
for _, key := range config.Section("files").Keys() {
|
||||||
if name := key.Name(); name != "html_templates" && name != "lang_files" {
|
if name := key.Name(); name != "html_templates" && name != "lang_files" {
|
||||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
key.SetValue(key.MustString(filepath.Join(dataPath, (key.Name() + ".json"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} {
|
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} {
|
||||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
config.Section("files").Key(key).SetValue(config.Section("files").Key(key).MustString(filepath.Join(dataPath, (key + ".json"))))
|
||||||
}
|
}
|
||||||
for _, key := range []string{"matrix_sql"} {
|
for _, key := range []string{"matrix_sql"} {
|
||||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".db"))))
|
config.Section("files").Key(key).SetValue(config.Section("files").Key(key).MustString(filepath.Join(dataPath, (key + ".db"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If true, app.ExternalDomain() will return one based on the reported Host (ideally reported in "Host" or "X-Forwarded-Host" by the reverse proxy), falling back to app.externalDomain if not set.
|
// If true, ExternalDomain() will return one based on the reported Host (ideally reported in "Host" or "X-Forwarded-Host" by the reverse proxy), falling back to externalDomain if not set.
|
||||||
app.UseProxyHost = app.config.Section("ui").Key("use_proxy_host").MustBool(false)
|
UseProxyHost = config.Section("ui").Key("use_proxy_host").MustBool(false)
|
||||||
app.externalURI = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/")
|
externalURI = strings.TrimSuffix(strings.TrimSuffix(config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/")
|
||||||
if !strings.HasSuffix(app.externalURI, PAGES.Base) {
|
if !strings.HasSuffix(externalURI, PAGES.Base) {
|
||||||
app.err.Println(lm.NoURLSuffix)
|
logs.err.Println(lm.NoURLSuffix)
|
||||||
}
|
}
|
||||||
if app.externalURI == "" {
|
if externalURI == "" {
|
||||||
if app.UseProxyHost {
|
if UseProxyHost {
|
||||||
app.err.Println(lm.NoExternalHost + lm.LoginWontSave + lm.SetExternalHostDespiteUseProxyHost)
|
logs.err.Println(lm.NoExternalHost + lm.LoginWontSave + lm.SetExternalHostDespiteUseProxyHost)
|
||||||
} else {
|
} else {
|
||||||
app.err.Println(lm.NoExternalHost + lm.LoginWontSave)
|
logs.err.Println(lm.NoExternalHost + lm.LoginWontSave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u, err := url.Parse(app.externalURI)
|
u, err := url.Parse(externalURI)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
app.externalDomain = u.Hostname()
|
externalDomain = u.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
config.Section("email").Key("no_username").SetValue(strconv.FormatBool(config.Section("email").Key("no_username").MustBool(false)))
|
||||||
|
|
||||||
app.MustSetValue("password_resets", "email_html", "jfa-go:"+"email.html")
|
// FIXME: Remove all these, eventually
|
||||||
app.MustSetValue("password_resets", "email_text", "jfa-go:"+"email.txt")
|
// config.MustSetValue("password_resets", "email_html", "jfa-go:"+"email.html")
|
||||||
|
// config.MustSetValue("password_resets", "email_text", "jfa-go:"+"email.txt")
|
||||||
|
|
||||||
app.MustSetValue("invite_emails", "email_html", "jfa-go:"+"invite-email.html")
|
// config.MustSetValue("invite_emails", "email_html", "jfa-go:"+"invite-email.html")
|
||||||
app.MustSetValue("invite_emails", "email_text", "jfa-go:"+"invite-email.txt")
|
// config.MustSetValue("invite_emails", "email_text", "jfa-go:"+"invite-email.txt")
|
||||||
|
|
||||||
app.MustSetValue("email_confirmation", "email_html", "jfa-go:"+"confirmation.html")
|
// config.MustSetValue("email_confirmation", "email_html", "jfa-go:"+"confirmation.html")
|
||||||
app.MustSetValue("email_confirmation", "email_text", "jfa-go:"+"confirmation.txt")
|
// config.MustSetValue("email_confirmation", "email_text", "jfa-go:"+"confirmation.txt")
|
||||||
|
|
||||||
app.MustSetValue("notifications", "expiry_html", "jfa-go:"+"expired.html")
|
// config.MustSetValue("notifications", "expiry_html", "jfa-go:"+"expired.html")
|
||||||
app.MustSetValue("notifications", "expiry_text", "jfa-go:"+"expired.txt")
|
// config.MustSetValue("notifications", "expiry_text", "jfa-go:"+"expired.txt")
|
||||||
|
|
||||||
app.MustSetValue("notifications", "created_html", "jfa-go:"+"created.html")
|
// config.MustSetValue("notifications", "created_html", "jfa-go:"+"created.html")
|
||||||
app.MustSetValue("notifications", "created_text", "jfa-go:"+"created.txt")
|
// config.MustSetValue("notifications", "created_text", "jfa-go:"+"created.txt")
|
||||||
|
|
||||||
app.MustSetValue("deletion", "email_html", "jfa-go:"+"deleted.html")
|
// config.MustSetValue("deletion", "email_html", "jfa-go:"+"deleted.html")
|
||||||
app.MustSetValue("deletion", "email_text", "jfa-go:"+"deleted.txt")
|
// config.MustSetValue("deletion", "email_text", "jfa-go:"+"deleted.txt")
|
||||||
|
|
||||||
app.MustSetValue("smtp", "hello_hostname", "localhost")
|
|
||||||
app.MustSetValue("smtp", "cert_validation", "true")
|
|
||||||
app.MustSetValue("smtp", "auth_type", "4")
|
|
||||||
app.MustSetValue("smtp", "port", "465")
|
|
||||||
|
|
||||||
app.MustSetValue("activity_log", "keep_n_records", "1000")
|
|
||||||
app.MustSetValue("activity_log", "delete_after_days", "90")
|
|
||||||
|
|
||||||
sc := app.config.Section("discord").Key("start_command").MustString("start")
|
|
||||||
app.config.Section("discord").Key("start_command").SetValue(strings.TrimPrefix(strings.TrimPrefix(sc, "/"), "!"))
|
|
||||||
|
|
||||||
// Deletion template is good enough for these as well.
|
// Deletion template is good enough for these as well.
|
||||||
app.MustSetValue("disable_enable", "disabled_html", "jfa-go:"+"deleted.html")
|
// config.MustSetValue("disable_enable", "disabled_html", "jfa-go:"+"deleted.html")
|
||||||
app.MustSetValue("disable_enable", "disabled_text", "jfa-go:"+"deleted.txt")
|
// config.MustSetValue("disable_enable", "disabled_text", "jfa-go:"+"deleted.txt")
|
||||||
app.MustSetValue("disable_enable", "enabled_html", "jfa-go:"+"deleted.html")
|
// config.MustSetValue("disable_enable", "enabled_html", "jfa-go:"+"deleted.html")
|
||||||
app.MustSetValue("disable_enable", "enabled_text", "jfa-go:"+"deleted.txt")
|
// config.MustSetValue("disable_enable", "enabled_text", "jfa-go:"+"deleted.txt")
|
||||||
|
|
||||||
app.MustSetValue("welcome_email", "email_html", "jfa-go:"+"welcome.html")
|
// config.MustSetValue("welcome_email", "email_html", "jfa-go:"+"welcome.html")
|
||||||
app.MustSetValue("welcome_email", "email_text", "jfa-go:"+"welcome.txt")
|
// config.MustSetValue("welcome_email", "email_text", "jfa-go:"+"welcome.txt")
|
||||||
|
|
||||||
app.MustSetValue("template_email", "email_html", "jfa-go:"+"template.html")
|
// config.MustSetValue("template_email", "email_html", "jfa-go:"+"template.html")
|
||||||
app.MustSetValue("template_email", "email_text", "jfa-go:"+"template.txt")
|
// config.MustSetValue("template_email", "email_text", "jfa-go:"+"template.txt")
|
||||||
|
|
||||||
app.MustSetValue("user_expiry", "behaviour", "disable_user")
|
config.MustSetValue("user_expiry", "behaviour", "disable_user")
|
||||||
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
|
// config.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
|
||||||
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
|
// config.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
|
||||||
|
|
||||||
app.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html")
|
// config.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html")
|
||||||
app.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt")
|
// config.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt")
|
||||||
|
|
||||||
app.MustSetValue("user_expiry", "reminder_email_html", "jfa-go:"+"expiry-reminder.html")
|
// config.MustSetValue("user_expiry", "reminder_email_html", "jfa-go:"+"expiry-reminder.html")
|
||||||
app.MustSetValue("user_expiry", "reminder_email_text", "jfa-go:"+"expiry-reminder.txt")
|
// config.MustSetValue("user_expiry", "reminder_email_text", "jfa-go:"+"expiry-reminder.txt")
|
||||||
|
|
||||||
app.MustSetValue("email", "collect", "true")
|
fnameSettingSuffix := []string{"html", "text"}
|
||||||
|
fnameExtension := []string{"html", "txt"}
|
||||||
|
|
||||||
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
|
for _, cc := range customContent {
|
||||||
app.MustSetValue("matrix", "show_on_reg", "true")
|
if cc.SourceFile.DefaultValue == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range fnameSettingSuffix {
|
||||||
|
config.MustSetValue(cc.SourceFile.Section, cc.SourceFile.SettingPrefix+fnameSettingSuffix[i], "jfa-go:"+cc.SourceFile.DefaultValue+"."+fnameExtension[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.MustSetValue("discord", "show_on_reg", "true")
|
config.MustSetValue("smtp", "hello_hostname", "localhost")
|
||||||
|
config.MustSetValue("smtp", "cert_validation", "true")
|
||||||
|
config.MustSetValue("smtp", "auth_type", "4")
|
||||||
|
config.MustSetValue("smtp", "port", "465")
|
||||||
|
|
||||||
app.MustSetValue("telegram", "show_on_reg", "true")
|
config.MustSetValue("activity_log", "keep_n_records", "1000")
|
||||||
|
config.MustSetValue("activity_log", "delete_after_days", "90")
|
||||||
|
|
||||||
app.MustSetValue("backups", "every_n_minutes", "1440")
|
sc := config.Section("discord").Key("start_command").MustString("start")
|
||||||
app.MustSetValue("backups", "path", filepath.Join(app.dataPath, "backups"))
|
config.Section("discord").Key("start_command").SetValue(strings.TrimPrefix(strings.TrimPrefix(sc, "/"), "!"))
|
||||||
app.MustSetValue("backups", "keep_n_backups", "20")
|
|
||||||
app.MustSetValue("backups", "keep_previous_version_backup", "true")
|
|
||||||
|
|
||||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
config.MustSetValue("email", "collect", "true")
|
||||||
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))
|
|
||||||
|
|
||||||
app.MustSetValue("jellyfin", "cache_timeout", "30")
|
config.MustSetValue("matrix", "topic", "Jellyfin notifications")
|
||||||
app.MustSetValue("jellyfin", "web_cache_async_timeout", "1")
|
config.MustSetValue("matrix", "show_on_reg", "true")
|
||||||
app.MustSetValue("jellyfin", "web_cache_sync_timeout", "10")
|
|
||||||
|
|
||||||
LOGIP = app.config.Section("advanced").Key("log_ips").MustBool(false)
|
config.MustSetValue("discord", "show_on_reg", "true")
|
||||||
LOGIPU = app.config.Section("advanced").Key("log_ips_users").MustBool(false)
|
|
||||||
|
|
||||||
app.MustSetValue("advanced", "auth_retry_count", "6")
|
config.MustSetValue("telegram", "show_on_reg", "true")
|
||||||
app.MustSetValue("advanced", "auth_retry_gap", "10")
|
|
||||||
|
|
||||||
app.MustSetValue("ui", "port", "8056")
|
config.MustSetValue("backups", "every_n_minutes", "1440")
|
||||||
app.MustSetValue("advanced", "tls_port", "8057")
|
config.MustSetValue("backups", "path", filepath.Join(dataPath, "backups"))
|
||||||
|
config.MustSetValue("backups", "keep_n_backups", "20")
|
||||||
|
config.MustSetValue("backups", "keep_previous_version_backup", "true")
|
||||||
|
|
||||||
app.MustSetValue("advanced", "value_log_size", "512")
|
config.Section("jellyfin").Key("version").SetValue(version)
|
||||||
|
config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||||
|
config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||||
|
|
||||||
|
config.MustSetValue("jellyfin", "cache_timeout", "30")
|
||||||
|
config.MustSetValue("jellyfin", "web_cache_async_timeout", "1")
|
||||||
|
config.MustSetValue("jellyfin", "web_cache_sync_timeout", "10")
|
||||||
|
|
||||||
|
LOGIP = config.Section("advanced").Key("log_ips").MustBool(false)
|
||||||
|
LOGIPU = config.Section("advanced").Key("log_ips_users").MustBool(false)
|
||||||
|
|
||||||
|
config.MustSetValue("advanced", "auth_retry_count", "6")
|
||||||
|
config.MustSetValue("advanced", "auth_retry_gap", "10")
|
||||||
|
|
||||||
|
config.MustSetValue("ui", "port", "8056")
|
||||||
|
config.MustSetValue("advanced", "tls_port", "8057")
|
||||||
|
|
||||||
|
config.MustSetValue("advanced", "value_log_size", "512")
|
||||||
|
|
||||||
pwrMethods := []string{"allow_pwr_username", "allow_pwr_email", "allow_pwr_contact_method"}
|
pwrMethods := []string{"allow_pwr_username", "allow_pwr_email", "allow_pwr_contact_method"}
|
||||||
allDisabled := true
|
allDisabled := true
|
||||||
for _, v := range pwrMethods {
|
for _, v := range pwrMethods {
|
||||||
if app.config.Section("user_page").Key(v).MustBool(true) {
|
if config.Section("user_page").Key(v).MustBool(true) {
|
||||||
allDisabled = false
|
allDisabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allDisabled {
|
if allDisabled {
|
||||||
app.info.Println(lm.EnableAllPWRMethods)
|
logs.info.Println(lm.EnableAllPWRMethods)
|
||||||
for _, v := range pwrMethods {
|
for _, v := range pwrMethods {
|
||||||
app.config.Section("user_page").Key(v).SetValue("true")
|
config.Section("user_page").Key(v).SetValue("true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
messagesEnabled = config.Section("messages").Key("enabled").MustBool(false)
|
||||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
telegramEnabled = config.Section("telegram").Key("enabled").MustBool(false)
|
||||||
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
|
discordEnabled = config.Section("discord").Key("enabled").MustBool(false)
|
||||||
matrixEnabled = app.config.Section("matrix").Key("enabled").MustBool(false)
|
matrixEnabled = config.Section("matrix").Key("enabled").MustBool(false)
|
||||||
if !messagesEnabled {
|
if !messagesEnabled {
|
||||||
emailEnabled = false
|
emailEnabled = false
|
||||||
telegramEnabled = false
|
telegramEnabled = false
|
||||||
discordEnabled = false
|
discordEnabled = false
|
||||||
matrixEnabled = false
|
matrixEnabled = false
|
||||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
} else if config.Section("email").Key("method").MustString("") == "" {
|
||||||
emailEnabled = false
|
emailEnabled = false
|
||||||
} else {
|
} else {
|
||||||
emailEnabled = true
|
emailEnabled = true
|
||||||
@@ -308,31 +330,64 @@ func (app *appContext) loadConfig() error {
|
|||||||
messagesEnabled = false
|
messagesEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.proxyEnabled = app.config.Section("advanced").Key("proxy").MustBool(false); app.proxyEnabled {
|
if proxyEnabled := config.Section("advanced").Key("proxy").MustBool(false); proxyEnabled {
|
||||||
app.proxyConfig = easyproxy.ProxyConfig{}
|
config.proxyConfig = &easyproxy.ProxyConfig{}
|
||||||
app.proxyConfig.Protocol = easyproxy.HTTP
|
config.proxyConfig.Protocol = easyproxy.HTTP
|
||||||
if strings.Contains(app.config.Section("advanced").Key("proxy_protocol").MustString("http"), "socks") {
|
if strings.Contains(config.Section("advanced").Key("proxy_protocol").MustString("http"), "socks") {
|
||||||
app.proxyConfig.Protocol = easyproxy.SOCKS5
|
config.proxyConfig.Protocol = easyproxy.SOCKS5
|
||||||
}
|
}
|
||||||
app.proxyConfig.Addr = app.config.Section("advanced").Key("proxy_address").MustString("")
|
config.proxyConfig.Addr = config.Section("advanced").Key("proxy_address").MustString("")
|
||||||
app.proxyConfig.User = app.config.Section("advanced").Key("proxy_user").MustString("")
|
config.proxyConfig.User = config.Section("advanced").Key("proxy_user").MustString("")
|
||||||
app.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("")
|
config.proxyConfig.Password = config.Section("advanced").Key("proxy_password").MustString("")
|
||||||
app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
|
config.proxyTransport, err = easyproxy.NewTransport(*(config.proxyConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedInitProxy, app.proxyConfig.Addr, err)
|
logs.err.Printf(lm.FailedInitProxy, config.proxyConfig.Addr, err)
|
||||||
// As explained in lm.FailedInitProxy, sleep here might grab the admin's attention,
|
// As explained in lm.FailedInitProxy, sleep here might grab the admin's attention,
|
||||||
// Since we don't crash on this failing.
|
// Since we don't crash on this failing.
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
app.proxyEnabled = false
|
config.proxyConfig = nil
|
||||||
|
config.proxyTransport = nil
|
||||||
} else {
|
} else {
|
||||||
app.proxyEnabled = true
|
logs.info.Printf(lm.InitProxy, config.proxyConfig.Addr)
|
||||||
app.info.Printf(lm.InitProxy, app.proxyConfig.Addr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.MustSetValue("updates", "enabled", "true")
|
config.MustSetValue("updates", "enabled", "true")
|
||||||
releaseChannel := app.config.Section("updates").Key("channel").String()
|
|
||||||
if app.config.Section("updates").Key("enabled").MustBool(false) {
|
substituteStrings = config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
|
||||||
|
|
||||||
|
if substituteStrings != "" {
|
||||||
|
v := config.Section("ui").Key("success_message")
|
||||||
|
v.SetValue(strings.ReplaceAll(v.String(), "Jellyfin", substituteStrings))
|
||||||
|
}
|
||||||
|
|
||||||
|
datePattern = config.Section("messages").Key("date_format").String()
|
||||||
|
timePattern = `%H:%M`
|
||||||
|
if !(config.Section("messages").Key("use_24h").MustBool(true)) {
|
||||||
|
timePattern = `%I:%M %p`
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadDependents re-initialises or applies changes to components of the app which can be reconfigured without restarting.
|
||||||
|
func (config *Config) ReloadDependents(app *appContext) {
|
||||||
|
oldFormLang := config.Section("ui").Key("language").MustString("")
|
||||||
|
if oldFormLang != "" {
|
||||||
|
app.storage.lang.chosenUserLang = oldFormLang
|
||||||
|
}
|
||||||
|
newFormLang := config.Section("ui").Key("language-form").MustString("")
|
||||||
|
if newFormLang != "" {
|
||||||
|
app.storage.lang.chosenUserLang = newFormLang
|
||||||
|
}
|
||||||
|
|
||||||
|
app.storage.lang.chosenAdminLang = config.Section("ui").Key("language-admin").MustString("en-us")
|
||||||
|
app.storage.lang.chosenEmailLang = config.Section("email").Key("language").MustString("en-us")
|
||||||
|
app.storage.lang.chosenPWRLang = config.Section("password_resets").Key("language").MustString("en-us")
|
||||||
|
app.storage.lang.chosenTelegramLang = config.Section("telegram").Key("language").MustString("en-us")
|
||||||
|
|
||||||
|
releaseChannel := config.Section("updates").Key("channel").String()
|
||||||
|
if config.Section("updates").Key("enabled").MustBool(false) {
|
||||||
v := version
|
v := version
|
||||||
if releaseChannel == "stable" {
|
if releaseChannel == "stable" {
|
||||||
if version == "git" {
|
if version == "git" {
|
||||||
@@ -341,9 +396,9 @@ func (app *appContext) loadConfig() error {
|
|||||||
} else if releaseChannel == "unstable" {
|
} else if releaseChannel == "unstable" {
|
||||||
v = "git"
|
v = "git"
|
||||||
}
|
}
|
||||||
app.updater = newUpdater(baseURL, namespace, repo, v, commit, updater)
|
app.updater = NewUpdater(baseURL, namespace, repo, v, commit, updater)
|
||||||
if app.proxyEnabled {
|
if config.proxyTransport != nil {
|
||||||
app.updater.SetTransport(app.proxyTransport)
|
app.updater.SetTransport(config.proxyTransport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if releaseChannel == "" {
|
if releaseChannel == "" {
|
||||||
@@ -352,32 +407,21 @@ func (app *appContext) loadConfig() error {
|
|||||||
} else {
|
} else {
|
||||||
releaseChannel = "stable"
|
releaseChannel = "stable"
|
||||||
}
|
}
|
||||||
app.MustSetValue("updates", "channel", releaseChannel)
|
config.MustSetValue("updates", "channel", releaseChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
|
app.email = NewEmailer(config, app.storage, app.LoggerSet)
|
||||||
|
}
|
||||||
|
|
||||||
if substituteStrings != "" {
|
func (app *appContext) ReloadConfig() {
|
||||||
v := app.config.Section("ui").Key("success_message")
|
var err error = nil
|
||||||
v.SetValue(strings.ReplaceAll(v.String(), "Jellyfin", substituteStrings))
|
app.config, err = NewConfig(app.configPath, app.dataPath, app.LoggerSet)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Fatalf(lm.FailedLoadConfig, app.configPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldFormLang := app.config.Section("ui").Key("language").MustString("")
|
app.config.ReloadDependents(app)
|
||||||
if oldFormLang != "" {
|
app.info.Printf(lm.LoadConfig, app.configPath)
|
||||||
app.storage.lang.chosenUserLang = oldFormLang
|
|
||||||
}
|
|
||||||
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
|
|
||||||
if newFormLang != "" {
|
|
||||||
app.storage.lang.chosenUserLang = newFormLang
|
|
||||||
}
|
|
||||||
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) PatchConfigBase() {
|
func (app *appContext) PatchConfigBase() {
|
||||||
|
|||||||
372
customcontent.go
Normal file
372
customcontent.go
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultVars(vars ...string) []string {
|
||||||
|
return slices.Concat(vars, []string{
|
||||||
|
"username",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultVals(vals map[string]any) map[string]any {
|
||||||
|
maps.Copy(vals, map[string]any{
|
||||||
|
"username": "Username",
|
||||||
|
})
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
var customContent = map[string]CustomContentInfo{
|
||||||
|
"EmailConfirmation": {
|
||||||
|
Name: "EmailConfirmation",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].EmailConfirmation["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("email_confirmation").Key("subject").MustString(lang.EmailConfirmation.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"confirmationURL",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"confirmationURL": "https://sub2.test.url/invite/xxxxxx?key=xxxxxx",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "email_confirmation",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
DefaultValue: "confirmation",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ExpiryReminder": {
|
||||||
|
Name: "ExpiryReminder",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].ExpiryReminder["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("user_expiry").Key("reminder_subject").MustString(lang.ExpiryReminder.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"expiresIn",
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"expiresIn": "3d",
|
||||||
|
"date": "20/08/25",
|
||||||
|
"time": "14:19",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "user_expiry",
|
||||||
|
SettingPrefix: "reminder_email_",
|
||||||
|
DefaultValue: "expiry-reminder",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"InviteEmail": {
|
||||||
|
Name: "InviteEmail",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].InviteEmail["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("invite_emails").Key("subject").MustString(lang.InviteEmail.get("title"))
|
||||||
|
},
|
||||||
|
Variables: []string{
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"expiresInMinutes",
|
||||||
|
"inviteURL",
|
||||||
|
},
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"date": "01/01/01",
|
||||||
|
"time": "00:00",
|
||||||
|
"expiresInMinutes": "16d 13h 19m",
|
||||||
|
"inviteURL": "https://sub2.test.url/invite/xxxxxx",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "invite_emails",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
DefaultValue: "invite-email",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"InviteExpiry": {
|
||||||
|
Name: "InviteExpiry",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].InviteExpiry["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return lang.InviteExpiry.get("title")
|
||||||
|
},
|
||||||
|
Variables: []string{
|
||||||
|
"code",
|
||||||
|
"time",
|
||||||
|
},
|
||||||
|
Placeholders: map[string]any{
|
||||||
|
"code": "\"xxxxxx\"",
|
||||||
|
"time": "01/01/01 00:00",
|
||||||
|
},
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "notifications",
|
||||||
|
SettingPrefix: "expiry_",
|
||||||
|
DefaultValue: "expired",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"PasswordReset": {
|
||||||
|
Name: "PasswordReset",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].PasswordReset["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("password_resets").Key("subject").MustString(lang.PasswordReset.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"expiresInMinutes",
|
||||||
|
"pin",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"date": "01/01/01",
|
||||||
|
"time": "00:00",
|
||||||
|
"expiresInMinutes": "16d 13h 19m",
|
||||||
|
"pin": "12-34-56",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "password_resets",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
// This was the first email type added, hence the undescriptive filename.
|
||||||
|
DefaultValue: "email",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserCreated": {
|
||||||
|
Name: "UserCreated",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].UserCreated["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return lang.UserCreated.get("title")
|
||||||
|
},
|
||||||
|
Variables: []string{
|
||||||
|
"code",
|
||||||
|
"name",
|
||||||
|
"address",
|
||||||
|
"time",
|
||||||
|
},
|
||||||
|
Placeholders: map[string]any{
|
||||||
|
"name": "Subject Username",
|
||||||
|
"code": "\"xxxxxx\"",
|
||||||
|
"address": "Email Address",
|
||||||
|
"time": "01/01/01 00:00",
|
||||||
|
},
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "notifications",
|
||||||
|
SettingPrefix: "created_",
|
||||||
|
DefaultValue: "created",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserDeleted": {
|
||||||
|
Name: "UserDeleted",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].UserDeleted["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("deletion").Key("subject").MustString(lang.UserDeleted.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"reason",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"reason": "Reason",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "deletion",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
DefaultValue: "deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserDisabled": {
|
||||||
|
Name: "UserDisabled",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].UserDisabled["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("disable_enable").Key("subject_disabled").MustString(lang.UserDisabled.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"reason",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"reason": "Reason",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "disable_enable",
|
||||||
|
SettingPrefix: "disabled_",
|
||||||
|
// Template is shared between deletion enabling and disabling.
|
||||||
|
DefaultValue: "deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserEnabled": {
|
||||||
|
Name: "UserEnabled",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].UserEnabled["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("disable_enable").Key("subject_enabled").MustString(lang.UserEnabled.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"reason",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"reason": "Reason",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "disable_enable",
|
||||||
|
SettingPrefix: "enabled_",
|
||||||
|
// Template is shared between deletion enabling and disabling.
|
||||||
|
DefaultValue: "deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserExpired": {
|
||||||
|
Name: "UserExpired",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].UserExpired["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("user_expiry").Key("subject").MustString(lang.UserExpired.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(),
|
||||||
|
Placeholders: defaultVals(map[string]any{}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "user_expiry",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
DefaultValue: "user-expired",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserExpiryAdjusted": {
|
||||||
|
Name: "UserExpiryAdjusted",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].UserExpiryAdjusted["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("user_expiry").Key("adjustment_subject").MustString(lang.UserExpiryAdjusted.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"newExpiry",
|
||||||
|
"reason",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"newExpiry": "",
|
||||||
|
"reason": "Reason",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "user_expiry",
|
||||||
|
SettingPrefix: "adjustment_email_",
|
||||||
|
DefaultValue: "expiry-adjusted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"WelcomeEmail": {
|
||||||
|
Name: "WelcomeEmail",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string { return dict.Email[lang].WelcomeEmail["name"] },
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return config.Section("welcome_email").Key("subject").MustString(lang.WelcomeEmail.get("title"))
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"jellyfinURL",
|
||||||
|
"yourAccountWillExpire",
|
||||||
|
),
|
||||||
|
Conditionals: []string{
|
||||||
|
"yourAccountWillExpire",
|
||||||
|
},
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"jellyfinURL": "https://example.io",
|
||||||
|
"yourAccountWillExpire": "17/08/25 14:19",
|
||||||
|
}),
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "welcome_email",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
DefaultValue: "welcome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"TemplateEmail": {
|
||||||
|
Name: "TemplateEmail",
|
||||||
|
DisplayName: func(dict *Lang, lang string) string {
|
||||||
|
return "EmptyCustomContent"
|
||||||
|
},
|
||||||
|
ContentType: CustomTemplate,
|
||||||
|
SourceFile: ContentSourceFileInfo{
|
||||||
|
Section: "template_email",
|
||||||
|
SettingPrefix: "email_",
|
||||||
|
DefaultValue: "template",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"UserLogin": {
|
||||||
|
Name: "UserLogin",
|
||||||
|
ContentType: CustomCard,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string {
|
||||||
|
if _, ok := dict.Admin[lang]; !ok {
|
||||||
|
lang = dict.chosenAdminLang
|
||||||
|
}
|
||||||
|
return dict.Admin[lang].Strings["userPageLogin"]
|
||||||
|
},
|
||||||
|
Variables: []string{},
|
||||||
|
},
|
||||||
|
"UserPage": {
|
||||||
|
Name: "UserPage",
|
||||||
|
ContentType: CustomCard,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string {
|
||||||
|
if _, ok := dict.Admin[lang]; !ok {
|
||||||
|
lang = dict.chosenAdminLang
|
||||||
|
}
|
||||||
|
return dict.Admin[lang].Strings["userPagePage"]
|
||||||
|
},
|
||||||
|
Variables: defaultVars(),
|
||||||
|
Placeholders: defaultVals(map[string]any{}),
|
||||||
|
},
|
||||||
|
"PostSignupCard": {
|
||||||
|
Name: "PostSignupCard",
|
||||||
|
ContentType: CustomCard,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string {
|
||||||
|
if _, ok := dict.Admin[lang]; !ok {
|
||||||
|
lang = dict.chosenAdminLang
|
||||||
|
}
|
||||||
|
return dict.Admin[lang].Strings["postSignupCard"]
|
||||||
|
},
|
||||||
|
Description: func(dict *Lang, lang string) string {
|
||||||
|
if _, ok := dict.Admin[lang]; !ok {
|
||||||
|
lang = dict.chosenAdminLang
|
||||||
|
}
|
||||||
|
return dict.Admin[lang].Strings["postSignupCardDescription"]
|
||||||
|
},
|
||||||
|
Variables: defaultVars(
|
||||||
|
"myAccountURL",
|
||||||
|
),
|
||||||
|
Placeholders: defaultVals(map[string]any{
|
||||||
|
"myAccountURL": "https://sub2.test.url/my/account",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var EmptyCustomContent = CustomContentInfo{
|
||||||
|
Name: "EmptyCustomContent",
|
||||||
|
ContentType: CustomMessage,
|
||||||
|
DisplayName: func(dict *Lang, lang string) string {
|
||||||
|
return "EmptyCustomContent"
|
||||||
|
},
|
||||||
|
Subject: func(config *Config, lang *emailLang) string {
|
||||||
|
return "EmptyCustomContent"
|
||||||
|
},
|
||||||
|
Description: nil,
|
||||||
|
Variables: []string{},
|
||||||
|
Placeholders: map[string]any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var AnnouncementCustomContent = func(subject string) CustomContentInfo {
|
||||||
|
cci := EmptyCustomContent
|
||||||
|
cci.Subject = func(config *Config, lang *emailLang) string { return subject }
|
||||||
|
cci.Variables = defaultVars()
|
||||||
|
cci.Placeholders = defaultVals(map[string]any{})
|
||||||
|
return cci
|
||||||
|
}
|
||||||
|
|
||||||
|
var _runtimeValidation = func() bool {
|
||||||
|
for name, cc := range customContent {
|
||||||
|
if name != cc.Name {
|
||||||
|
panic(fmt.Errorf("customContent key and name not matching: %s != %s", name, cc.Name))
|
||||||
|
}
|
||||||
|
if cc.DisplayName == nil {
|
||||||
|
panic(fmt.Errorf("no customContent[%s] DisplayName set", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}()
|
||||||
@@ -735,7 +735,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
|
|
||||||
var msg *Message
|
var msg *Message
|
||||||
if err == nil {
|
if err == nil {
|
||||||
msg, err = d.app.email.constructInvite(invite.Code, invite, d.app, false)
|
msg, err = d.app.email.constructInvite(invite, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Print extra message, ideally we'd just print this, or get rid of it though.
|
// Print extra message, ideally we'd just print this, or get rid of it though.
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
|
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||||
|
|||||||
490
email_test.go
Normal file
490
email_test.go
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/hrfee/jfa-go/logger"
|
||||||
|
"github.com/lithammer/shortuuid/v3"
|
||||||
|
"github.com/timshannon/badgerhold/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed build/data/config-default.ini
|
||||||
|
var configFS embed.FS
|
||||||
|
var db *badgerhold.Store
|
||||||
|
|
||||||
|
func dbClose(e *Emailer) {
|
||||||
|
e.storage.db.Close()
|
||||||
|
e.storage.db = nil
|
||||||
|
db = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(err any) {
|
||||||
|
fmt.Printf("Fatal log function called: %+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestEmailer initialises most of what the emailer depends on, which happens to be most of the app.
|
||||||
|
func NewTestEmailer() (*Emailer, error) {
|
||||||
|
if binaryType != "external" {
|
||||||
|
return nil, errors.New("test only supported with -tags \"external\"")
|
||||||
|
}
|
||||||
|
emailer := &Emailer{
|
||||||
|
fromAddr: "from@addr",
|
||||||
|
fromName: "fromName",
|
||||||
|
LoggerSet: LoggerSet{
|
||||||
|
info: logger.NewLogger(os.Stdout, "[TEST INFO] ", log.Ltime, color.FgHiWhite),
|
||||||
|
err: logger.NewLogger(os.Stdout, "[TEST ERROR] ", log.Ltime|log.Lshortfile, color.FgRed),
|
||||||
|
debug: logger.NewLogger(os.Stdout, "[TEST DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow),
|
||||||
|
},
|
||||||
|
sender: &DummyClient{},
|
||||||
|
}
|
||||||
|
dConfig, err := fs.ReadFile(configFS, "build/data/config-default.ini")
|
||||||
|
if err != nil {
|
||||||
|
return emailer, err
|
||||||
|
}
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return emailer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force emailer to construct markdown
|
||||||
|
discordEnabled = true
|
||||||
|
// Use working directory
|
||||||
|
localFS = dirFS(filepath.Join(wd, "build", "data"))
|
||||||
|
langFS = dirFS(filepath.Join(wd, "build", "data", "lang"))
|
||||||
|
noInfoLS := emailer.LoggerSet
|
||||||
|
noInfoLS.info = logger.NewEmptyLogger()
|
||||||
|
emailer.config, err = NewConfig(dConfig, "/tmp/jfa-go-test", noInfoLS)
|
||||||
|
if err != nil {
|
||||||
|
return emailer, err
|
||||||
|
}
|
||||||
|
emailer.storage = NewStorage("/tmp/db", emailer.debug, func(k string) DebugLogAction { return LogAll })
|
||||||
|
emailer.storage.loadLang(langFS)
|
||||||
|
|
||||||
|
emailer.storage.lang.chosenAdminLang = emailer.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||||
|
emailer.storage.lang.chosenEmailLang = emailer.config.Section("email").Key("language").MustString("en-us")
|
||||||
|
emailer.storage.lang.chosenPWRLang = emailer.config.Section("password_resets").Key("language").MustString("en-us")
|
||||||
|
emailer.storage.lang.chosenTelegramLang = emailer.config.Section("telegram").Key("language").MustString("en-us")
|
||||||
|
|
||||||
|
opts := badgerhold.DefaultOptions
|
||||||
|
opts.Dir = "/tmp/jfa-go-test-db"
|
||||||
|
opts.ValueDir = opts.Dir
|
||||||
|
opts.SyncWrites = false
|
||||||
|
opts.Logger = nil
|
||||||
|
emailer.storage.db, err = badgerhold.Open(opts)
|
||||||
|
// emailer.info.Printf("DB Opened")
|
||||||
|
db = emailer.storage.db
|
||||||
|
if err != nil {
|
||||||
|
return emailer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
emailer.lang = emailer.storage.lang.Email[emailer.storage.lang.chosenEmailLang]
|
||||||
|
emailer.info.SetFatalFunc(Fatal)
|
||||||
|
emailer.err.SetFatalFunc(Fatal)
|
||||||
|
return emailer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDummyEmailerInit(t *testing.T) *Emailer {
|
||||||
|
e, err := NewTestEmailer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDummyEmailerInit(t *testing.T) {
|
||||||
|
dbClose(testDummyEmailerInit(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContent(e *Emailer, cci CustomContentInfo, t *testing.T, testFunc func(t *testing.T)) {
|
||||||
|
e.storage.DeleteCustomContentKey(cci.Name)
|
||||||
|
t.Run(cci.Name, testFunc)
|
||||||
|
cc := CustomContent{
|
||||||
|
Name: cci.Name,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
cc.Content = "start test content "
|
||||||
|
for _, v := range cci.Variables {
|
||||||
|
cc.Content += "{" + v + "}"
|
||||||
|
}
|
||||||
|
cc.Content += " end test content"
|
||||||
|
e.storage.SetCustomContentKey(cci.Name, cc)
|
||||||
|
t.Run(cci.Name+" Custom", testFunc)
|
||||||
|
e.storage.DeleteCustomContentKey(cci.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructConfirmation(code, username, key string, placeholders bool)
|
||||||
|
func TestConfirmation(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
// non-blank key, link should therefore not be a /my/confirm one
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
testContent(e, customContent["EmailConfirmation"], t, func(t *testing.T) {
|
||||||
|
code := shortuuid.New()
|
||||||
|
username := shortuuid.New()
|
||||||
|
key := shortuuid.New()
|
||||||
|
msg, err := e.constructConfirmation(code, username, key, false)
|
||||||
|
t.Run("FromInvite", func(t *testing.T) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if strings.Contains(content, "/my/confirm") {
|
||||||
|
t.Fatalf("/my/confirm link generated instead of invite confirm link: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, code) {
|
||||||
|
t.Fatalf("code not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, key) {
|
||||||
|
t.Fatalf("key not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
code = ""
|
||||||
|
msg, err = e.constructConfirmation(code, username, key, false)
|
||||||
|
t.Run("FromMyAccount", func(t *testing.T) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, "/my/confirm") {
|
||||||
|
t.Fatalf("/my/confirm link not generated: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, key) {
|
||||||
|
t.Fatalf("key not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructInvite(invite Invite, placeholders bool)
|
||||||
|
func TestInvite(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["InviteEmail"], t, func(t *testing.T) {
|
||||||
|
inv := Invite{
|
||||||
|
Code: shortuuid.New(),
|
||||||
|
Created: time.Now(),
|
||||||
|
ValidTill: time.Now().Add(30 * time.Minute),
|
||||||
|
}
|
||||||
|
msg, err := e.constructInvite(inv, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, inv.Code) {
|
||||||
|
t.Fatalf("code not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "30m") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructExpiry(code string, invite Invite, placeholders bool)
|
||||||
|
func TestExpiry(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["InviteExpiry"], t, func(t *testing.T) {
|
||||||
|
inv := Invite{
|
||||||
|
Code: shortuuid.New(),
|
||||||
|
Created: time.Time{},
|
||||||
|
ValidTill: time.Date(2025, 1, 2, 8, 37, 1, 1, time.UTC),
|
||||||
|
}
|
||||||
|
// So we can easily check is the expiry time is included (which is 0001-01-01).
|
||||||
|
for strings.Contains(inv.Code, "1") {
|
||||||
|
inv.Code = shortuuid.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := e.constructExpiry(inv, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, inv.Code) {
|
||||||
|
t.Fatalf("code not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "02/01/25") || !strings.Contains(content, "08:37") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructCreated(code, username, address string, invite Invite, placeholders bool)
|
||||||
|
func TestCreated(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["UserCreated"], t, func(t *testing.T) {
|
||||||
|
inv := Invite{
|
||||||
|
Code: shortuuid.New(),
|
||||||
|
Created: time.Time{},
|
||||||
|
ValidTill: time.Date(2025, 1, 2, 8, 37, 1, 1, time.UTC),
|
||||||
|
}
|
||||||
|
username := shortuuid.New()
|
||||||
|
address := shortuuid.New()
|
||||||
|
|
||||||
|
msg, err := e.constructCreated(username, address, inv.ValidTill, inv, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, inv.Code) {
|
||||||
|
t.Fatalf("code not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, address) {
|
||||||
|
t.Fatalf("address not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "02/01/25") || !strings.Contains(content, "08:37") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructReset(pwr PasswordReset, placeholders bool)
|
||||||
|
func TestReset(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["PasswordReset"], t, func(t *testing.T) {
|
||||||
|
pwr := PasswordReset{
|
||||||
|
Pin: shortuuid.New(),
|
||||||
|
Username: shortuuid.New(),
|
||||||
|
Expiry: time.Date(2025, 1, 2, 8, 37, 1, 1, time.UTC),
|
||||||
|
Internal: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := e.constructReset(pwr, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, pwr.Pin) {
|
||||||
|
t.Fatalf("pin not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, pwr.Username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "02/01/25") || !strings.Contains(content, "08:37") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructDeleted(reason string, placeholders bool)
|
||||||
|
func TestDeleted(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
testContent(e, customContent["UserDeleted"], t, func(t *testing.T) {
|
||||||
|
reason := shortuuid.New()
|
||||||
|
msg, err := e.constructDeleted(reason, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, reason) {
|
||||||
|
t.Fatalf("reason n)ot found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructDisabled(reason string, placeholders bool)
|
||||||
|
func TestDisabled(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
testContent(e, customContent["UserDeleted"], t, func(t *testing.T) {
|
||||||
|
reason := shortuuid.New()
|
||||||
|
msg, err := e.constructDisabled(reason, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, reason) {
|
||||||
|
t.Fatalf("reason not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructEnabled(reason string, placeholders bool)
|
||||||
|
func TestEnabled(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
testContent(e, customContent["UserDeleted"], t, func(t *testing.T) {
|
||||||
|
reason := shortuuid.New()
|
||||||
|
msg, err := e.constructEnabled(reason, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, reason) {
|
||||||
|
t.Fatalf("reason not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructExpiryAdjusted(username string, expiry time.Time, reason string, placeholders bool)
|
||||||
|
func TestExpiryAdjusted(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["UserExpiryAdjusted"], t, func(t *testing.T) {
|
||||||
|
username := shortuuid.New()
|
||||||
|
expiry := time.Date(2025, 1, 2, 8, 37, 1, 1, time.UTC)
|
||||||
|
reason := shortuuid.New()
|
||||||
|
msg, err := e.constructExpiryAdjusted(username, expiry, reason, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, reason) {
|
||||||
|
t.Fatalf("reason not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "02/01/25") || !strings.Contains(content, "08:37") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructExpiryReminder(username string, expiry time.Time, placeholders bool)
|
||||||
|
func TestExpiryReminder(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["ExpiryReminder"], t, func(t *testing.T) {
|
||||||
|
username := shortuuid.New()
|
||||||
|
expiry := time.Date(2025, 1, 2, 8, 37, 1, 1, time.UTC)
|
||||||
|
msg, err := e.constructExpiryReminder(username, expiry, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "02/01/25") || !strings.Contains(content, "08:37") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructWelcome(username string, expiry time.Time, placeholders bool)
|
||||||
|
func TestWelcome(t *testing.T) {
|
||||||
|
e := testDummyEmailerInit(t)
|
||||||
|
defer dbClose(e)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatalf("db nil")
|
||||||
|
}
|
||||||
|
// Fix date/time format
|
||||||
|
datePattern = "%d/%m/%y"
|
||||||
|
timePattern = "%H:%M"
|
||||||
|
testContent(e, customContent["WelcomeEmail"], t, func(t *testing.T) {
|
||||||
|
username := shortuuid.New()
|
||||||
|
expiry := time.Date(2025, 1, 2, 8, 37, 1, 1, time.UTC)
|
||||||
|
msg, err := e.constructWelcome(username, expiry, false)
|
||||||
|
t.Run("NoExpiry", func(t *testing.T) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
// time.Time{} is 0001-01-01... so look for a 1 in there at least.
|
||||||
|
if !strings.Contains(content, "02/01/25") || !strings.Contains(content, "08:37") {
|
||||||
|
t.Fatalf("expiry not found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
username = shortuuid.New()
|
||||||
|
expiry = time.Time{}
|
||||||
|
msg, err = e.constructWelcome(username, expiry, false)
|
||||||
|
t.Run("WithExpiry", func(t *testing.T) {
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed construct: %+v", err)
|
||||||
|
}
|
||||||
|
for _, content := range []string{msg.Text, msg.HTML} {
|
||||||
|
if !strings.Contains(content, username) {
|
||||||
|
t.Fatalf("username not found in output: %s", content)
|
||||||
|
}
|
||||||
|
if strings.Contains(content, "01/01/01") || strings.Contains(content, "00:00") {
|
||||||
|
t.Fatalf("empty expiry found in output: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
18
external.go
18
external.go
@@ -4,7 +4,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -15,9 +14,6 @@ const binaryType = "external"
|
|||||||
|
|
||||||
func BuildTagsExternal() { buildTags = append(buildTags, "external") }
|
func BuildTagsExternal() { buildTags = append(buildTags, "external") }
|
||||||
|
|
||||||
var localFS dirFS
|
|
||||||
var langFS dirFS
|
|
||||||
|
|
||||||
// When using os.DirFS, even on Windows the separator seems to be '/'.
|
// When using os.DirFS, even on Windows the separator seems to be '/'.
|
||||||
// func FSJoin(elem ...string) string { return filepath.Join(elem...) }
|
// func FSJoin(elem ...string) string { return filepath.Join(elem...) }
|
||||||
func FSJoin(elem ...string) string {
|
func FSJoin(elem ...string) string {
|
||||||
@@ -32,20 +28,6 @@ func FSJoin(elem ...string) string {
|
|||||||
return strings.TrimSuffix(path, sep)
|
return strings.TrimSuffix(path, sep)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dirFS string
|
|
||||||
|
|
||||||
func (dir dirFS) Open(name string) (fs.File, error) {
|
|
||||||
return os.Open(string(dir) + "/" + name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dir dirFS) ReadFile(name string) ([]byte, error) {
|
|
||||||
return os.ReadFile(string(dir) + "/" + name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dir dirFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
|
||||||
return os.ReadDir(string(dir) + "/" + name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadFilesystems() {
|
func loadFilesystems() {
|
||||||
log.Println("Using external storage")
|
log.Println("Using external storage")
|
||||||
executable, _ := os.Executable()
|
executable, _ := os.Executable()
|
||||||
|
|||||||
29
fs.go
Normal file
29
fs.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type genericFS interface {
|
||||||
|
fs.FS
|
||||||
|
fs.ReadDirFS
|
||||||
|
fs.ReadFileFS
|
||||||
|
}
|
||||||
|
|
||||||
|
var localFS genericFS
|
||||||
|
var langFS genericFS
|
||||||
|
|
||||||
|
type dirFS string
|
||||||
|
|
||||||
|
func (dir dirFS) Open(name string) (fs.File, error) {
|
||||||
|
return os.Open(string(dir) + "/" + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir dirFS) ReadFile(name string) ([]byte, error) {
|
||||||
|
return os.ReadFile(string(dir) + "/" + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir dirFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||||
|
return os.ReadDir(string(dir) + "/" + name)
|
||||||
|
}
|
||||||
@@ -19,9 +19,6 @@ var loFS embed.FS
|
|||||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram
|
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram
|
||||||
var laFS embed.FS
|
var laFS embed.FS
|
||||||
|
|
||||||
var langFS rewriteFS
|
|
||||||
var localFS rewriteFS
|
|
||||||
|
|
||||||
type rewriteFS struct {
|
type rewriteFS struct {
|
||||||
fs embed.FS
|
fs embed.FS
|
||||||
prefix string
|
prefix string
|
||||||
|
|||||||
10
lang.go
10
lang.go
@@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/hrfee/jfa-go/common"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
)
|
||||||
|
|
||||||
type langMeta struct {
|
type langMeta struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -166,7 +170,7 @@ func (ts *telegramLangs) getOptions() []common.Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type langSection map[string]string
|
type langSection map[string]string
|
||||||
type tmpl map[string]string
|
type tmpl = map[string]any
|
||||||
|
|
||||||
func templateString(text string, vals tmpl) string {
|
func templateString(text string, vals tmpl) string {
|
||||||
start, previousEnd := -1, -1
|
start, previousEnd := -1, -1
|
||||||
@@ -183,7 +187,7 @@ func templateString(text string, vals tmpl) string {
|
|||||||
start = -1
|
start = -1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out += text[previousEnd+1:start] + val
|
out += text[previousEnd+1:start] + fmt.Sprint(val)
|
||||||
previousEnd = i
|
previousEnd = i
|
||||||
start = -1
|
start = -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
empty bool
|
Empty bool
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
shortfile bool
|
shortfile bool
|
||||||
printer *c.Color
|
printer *c.Color
|
||||||
@@ -75,13 +75,13 @@ func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l *Lo
|
|||||||
|
|
||||||
func NewEmptyLogger() (l *Logger) {
|
func NewEmptyLogger() (l *Logger) {
|
||||||
l = &Logger{
|
l = &Logger{
|
||||||
empty: true,
|
Empty: true,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Printf(format string, v ...interface{}) {
|
func (l *Logger) Printf(format string, v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out string
|
var out string
|
||||||
@@ -93,7 +93,7 @@ func (l *Logger) Printf(format string, v ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) PrintfCustomLevel(level int, format string, v ...interface{}) {
|
func (l *Logger) PrintfCustomLevel(level int, format string, v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out string
|
var out string
|
||||||
@@ -105,14 +105,14 @@ func (l *Logger) PrintfCustomLevel(level int, format string, v ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) PrintfNoFile(format string, v ...interface{}) {
|
func (l *Logger) PrintfNoFile(format string, v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.logger.Print(l.printer.Sprintf(format, v...))
|
l.logger.Print(l.printer.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Print(v ...interface{}) {
|
func (l *Logger) Print(v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out string
|
var out string
|
||||||
@@ -124,7 +124,7 @@ func (l *Logger) Print(v ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Println(v ...interface{}) {
|
func (l *Logger) Println(v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out string
|
var out string
|
||||||
@@ -136,7 +136,7 @@ func (l *Logger) Println(v ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Fatal(v ...interface{}) {
|
func (l *Logger) Fatal(v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out string
|
var out string
|
||||||
@@ -148,7 +148,7 @@ func (l *Logger) Fatal(v ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Fatalf(format string, v ...interface{}) {
|
func (l *Logger) Fatalf(format string, v ...interface{}) {
|
||||||
if l.empty {
|
if l.Empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out string
|
var out string
|
||||||
|
|||||||
121
main.go
121
main.go
@@ -24,7 +24,6 @@ import (
|
|||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
_ "github.com/hrfee/jfa-go/docs"
|
_ "github.com/hrfee/jfa-go/docs"
|
||||||
"github.com/hrfee/jfa-go/easyproxy"
|
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
"github.com/hrfee/jfa-go/logger"
|
"github.com/hrfee/jfa-go/logger"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
@@ -81,6 +80,11 @@ var serverTypes = map[string]string{
|
|||||||
var serverType = mediabrowser.JellyfinServer
|
var serverType = mediabrowser.JellyfinServer
|
||||||
var substituteStrings = ""
|
var substituteStrings = ""
|
||||||
|
|
||||||
|
var externalURI, externalDomain string // The latter lower-case as should be accessed through app.ExternalDomain()
|
||||||
|
var UseProxyHost bool
|
||||||
|
|
||||||
|
var datePattern, timePattern string
|
||||||
|
|
||||||
// User is used for auth purposes.
|
// User is used for auth purposes.
|
||||||
type User struct {
|
type User struct {
|
||||||
UserID string `json:"id"`
|
UserID string `json:"id"`
|
||||||
@@ -88,10 +92,15 @@ type User struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set of the usual log channels, for ease of passing between things.
|
||||||
|
type LoggerSet struct {
|
||||||
|
info, debug, err *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
// contains (almost) everything the application needs, essentially. This was a dumb design decision imo.
|
// contains (almost) everything the application needs, essentially. This was a dumb design decision imo.
|
||||||
type appContext struct {
|
type appContext struct {
|
||||||
// defaults *Config
|
// defaults *Config
|
||||||
config *ini.File
|
config *Config
|
||||||
configPath string
|
configPath string
|
||||||
configBasePath string
|
configBasePath string
|
||||||
configBase common.Config
|
configBase common.Config
|
||||||
@@ -103,39 +112,32 @@ type appContext struct {
|
|||||||
adminUsers []User
|
adminUsers []User
|
||||||
invalidTokens []string
|
invalidTokens []string
|
||||||
// Keeping jf name because I can't think of a better one
|
// Keeping jf name because I can't think of a better one
|
||||||
jf *mediabrowser.MediaBrowser
|
jf *mediabrowser.MediaBrowser
|
||||||
authJf *mediabrowser.MediaBrowser
|
authJf *mediabrowser.MediaBrowser
|
||||||
ombi *OmbiWrapper
|
ombi *OmbiWrapper
|
||||||
js *JellyseerrWrapper
|
js *JellyseerrWrapper
|
||||||
thirdPartyServices []ThirdPartyService
|
thirdPartyServices []ThirdPartyService
|
||||||
datePattern string
|
storage *Storage
|
||||||
timePattern string
|
validator Validator
|
||||||
storage Storage
|
email *Emailer
|
||||||
validator Validator
|
telegram *TelegramDaemon
|
||||||
email *Emailer
|
discord *DiscordDaemon
|
||||||
telegram *TelegramDaemon
|
matrix *MatrixDaemon
|
||||||
discord *DiscordDaemon
|
contactMethods []ContactMethodLinker
|
||||||
matrix *MatrixDaemon
|
LoggerSet
|
||||||
contactMethods []ContactMethodLinker
|
host string
|
||||||
info, debug, err *logger.Logger
|
port int
|
||||||
host string
|
version string
|
||||||
port int
|
updater *Updater
|
||||||
version string
|
webhooks *WebhookSender
|
||||||
externalURI, externalDomain string // The latter lower-case as should be accessed through app.ExternalDomain()
|
newUpdate bool // Whether whatever's in update is new.
|
||||||
UseProxyHost bool
|
tag Tag
|
||||||
updater *Updater
|
update Update
|
||||||
webhooks *WebhookSender
|
internalPWRs map[string]InternalPWR
|
||||||
newUpdate bool // Whether whatever's in update is new.
|
pwrCaptchas map[string]Captcha
|
||||||
tag Tag
|
ConfirmationKeys map[string]map[string]ConfirmationKey // Map of invite code to jwt to request
|
||||||
update Update
|
confirmationKeysLock sync.Mutex
|
||||||
proxyEnabled bool
|
userCache *UserCache
|
||||||
proxyTransport *http.Transport
|
|
||||||
proxyConfig easyproxy.ProxyConfig
|
|
||||||
internalPWRs map[string]InternalPWR
|
|
||||||
pwrCaptchas map[string]Captcha
|
|
||||||
ConfirmationKeys map[string]map[string]ConfirmationKey // Map of invite code to jwt to request
|
|
||||||
confirmationKeysLock sync.Mutex
|
|
||||||
userCache *UserCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSecret(length int) (string, error) {
|
func generateSecret(length int) (string, error) {
|
||||||
@@ -244,7 +246,9 @@ func start(asDaemon, firstCall bool) {
|
|||||||
|
|
||||||
var debugMode bool
|
var debugMode bool
|
||||||
var address string
|
var address string
|
||||||
if err := app.loadConfig(); err != nil {
|
var err error = nil
|
||||||
|
app.config, err = NewConfig(app.configPath, app.dataPath, app.LoggerSet)
|
||||||
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedLoadConfig, app.configPath, err)
|
app.err.Fatalf(lm.FailedLoadConfig, app.configPath, err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.LoadConfig, app.configPath)
|
app.info.Printf(lm.LoadConfig, app.configPath)
|
||||||
@@ -262,12 +266,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
if debugMode {
|
if debugMode {
|
||||||
app.debug = logger.NewLogger(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow)
|
app.debug = logger.NewLogger(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow)
|
||||||
// Bind debug log
|
|
||||||
app.storage.debug = app.debug
|
|
||||||
app.storage.logActions = generateLogActions(app.config)
|
|
||||||
} else {
|
} else {
|
||||||
app.debug = logger.NewEmptyLogger()
|
app.debug = logger.NewEmptyLogger()
|
||||||
app.storage.debug = nil
|
|
||||||
}
|
}
|
||||||
if *PPROF {
|
if *PPROF {
|
||||||
app.info.Print(warning("\n\nWARNING: Don't use pprof in production.\n\n"))
|
app.info.Print(warning("\n\nWARNING: Don't use pprof in production.\n\n"))
|
||||||
@@ -312,14 +312,17 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.lang.CommonPath = "common"
|
dbPath := filepath.Join(app.dataPath, "db")
|
||||||
app.storage.lang.UserPath = "form"
|
if debugMode {
|
||||||
app.storage.lang.AdminPath = "admin"
|
app.storage = NewStorage(dbPath, app.debug, generateLogActions(app.config))
|
||||||
app.storage.lang.EmailPath = "email"
|
} else {
|
||||||
app.storage.lang.TelegramPath = "telegram"
|
app.storage = NewStorage(dbPath, app.debug, nil)
|
||||||
app.storage.lang.PasswordResetPath = "pwreset"
|
}
|
||||||
|
|
||||||
|
// Placed here, since storage.chosenXLang is set by this function.
|
||||||
|
app.config.ReloadDependents(app)
|
||||||
|
|
||||||
externalLang := app.config.Section("files").Key("lang_files").MustString("")
|
externalLang := app.config.Section("files").Key("lang_files").MustString("")
|
||||||
var err error
|
|
||||||
if externalLang == "" {
|
if externalLang == "" {
|
||||||
err = app.storage.loadLang(langFS)
|
err = app.storage.loadLang(langFS)
|
||||||
} else {
|
} else {
|
||||||
@@ -362,7 +365,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||||
|
|
||||||
// NOTE: As of writing this, the order in app.thirdPartServices doesn't matter,
|
// NOTE: As of writing this, the order in app.thirdPartyServices doesn't matter,
|
||||||
// but in future it might (like app.contactMethods does), so append to the end!
|
// but in future it might (like app.contactMethods does), so append to the end!
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
app.ombi = &OmbiWrapper{}
|
app.ombi = &OmbiWrapper{}
|
||||||
@@ -391,10 +394,12 @@ func start(asDaemon, firstCall bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.db_path = filepath.Join(app.dataPath, "db")
|
|
||||||
app.loadPendingBackup()
|
app.loadPendingBackup()
|
||||||
app.ConnectDB()
|
if err := app.storage.Connect(app.config); err != nil {
|
||||||
defer app.storage.db.Close()
|
app.err.Fatalf(lm.FailedConnectDB, dbPath, err)
|
||||||
|
}
|
||||||
|
app.info.Printf(lm.ConnectDB, dbPath)
|
||||||
|
defer app.storage.Close()
|
||||||
|
|
||||||
// copy it to app.patchedConfig, and patch in settings from app.config, and language stuff.
|
// copy it to app.patchedConfig, and patch in settings from app.config, and language stuff.
|
||||||
app.PatchConfigBase()
|
app.PatchConfigBase()
|
||||||
@@ -475,10 +480,9 @@ func start(asDaemon, firstCall bool) {
|
|||||||
time.Minute*time.Duration(app.config.Section("jellyfin").Key("web_cache_sync_timeout").MustInt()),
|
time.Minute*time.Duration(app.config.Section("jellyfin").Key("web_cache_sync_timeout").MustInt()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Since email depends on language, the email reload in loadConfig won't work first time.
|
// Since email depends on language, the email reload in NewConfig won't work first time.
|
||||||
// Email also handles its own proxying, as (SMTP atleast) doesn't use a HTTP transport.
|
// Email also handles its own proxying, as (SMTP atleast) doesn't use a HTTP transport.
|
||||||
app.email = NewEmailer(app)
|
app.email = NewEmailer(app.config, app.storage, app.LoggerSet)
|
||||||
app.loadStrftime()
|
|
||||||
|
|
||||||
var validatorConf ValidatorConf
|
var validatorConf ValidatorConf
|
||||||
|
|
||||||
@@ -579,13 +583,13 @@ func start(asDaemon, firstCall bool) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Updater proxy set in config.go, don't worry!
|
// Updater proxy set in config.go, don't worry!
|
||||||
if app.proxyEnabled {
|
if app.config.proxyConfig != nil {
|
||||||
app.jf.SetTransport(app.proxyTransport)
|
app.jf.SetTransport(app.config.proxyTransport)
|
||||||
for _, c := range app.thirdPartyServices {
|
for _, c := range app.thirdPartyServices {
|
||||||
c.SetTransport(app.proxyTransport)
|
c.SetTransport(app.config.proxyTransport)
|
||||||
}
|
}
|
||||||
for _, c := range app.contactMethods {
|
for _, c := range app.contactMethods {
|
||||||
c.SetTransport(app.proxyTransport)
|
c.SetTransport(app.config.proxyTransport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -601,7 +605,6 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.host = "0.0.0.0"
|
app.host = "0.0.0.0"
|
||||||
}
|
}
|
||||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||||
app.storage.lang.SetupPath = "setup"
|
|
||||||
err := app.storage.loadLangSetup(langFS)
|
err := app.storage.loadLangSetup(langFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.info.Fatalf(lm.FailedLangLoad, err)
|
app.info.Fatalf(lm.FailedLangLoad, err)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func migrateEmailConfig(app *appContext) {
|
|||||||
app.err.Fatalf("Failed to save config: %v", err)
|
app.err.Fatalf("Failed to save config: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.loadConfig()
|
app.ReloadConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate pre-0.3.6 email settings to the new messages section.
|
// Migrate pre-0.3.6 email settings to the new messages section.
|
||||||
@@ -245,7 +245,7 @@ func loadLegacyData(app *appContext) {
|
|||||||
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
|
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
|
||||||
app.storage.loadCustomEmails()
|
app.storage.loadCustomEmails()
|
||||||
|
|
||||||
app.MustSetValue("user_page", "enabled", "true")
|
app.config.MustSetValue("user_page", "enabled", "true")
|
||||||
if app.config.Section("user_page").Key("enabled").MustBool(false) {
|
if app.config.Section("user_page").Key("enabled").MustBool(false) {
|
||||||
app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String()
|
app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String()
|
||||||
app.storage.loadUserPageContent()
|
app.storage.loadUserPageContent()
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenResetLink generates and returns a password reset link.
|
// GenResetLink generates and returns a password reset link.
|
||||||
func (app *appContext) GenResetLink(pin string) (string, error) {
|
func GenResetLink(pin string) (string, error) {
|
||||||
url := app.ExternalURI(nil)
|
url := ExternalURI(nil)
|
||||||
var pinLink string
|
var pinLink string
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return pinLink, errors.New(lm.NoExternalHost)
|
return pinLink, errors.New(lm.NoExternalHost)
|
||||||
@@ -104,7 +104,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
|||||||
uid := user.ID
|
uid := user.ID
|
||||||
name := app.getAddressOrName(uid)
|
name := app.getAddressOrName(uid)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
msg, err := app.email.constructReset(pwr, app, false)
|
msg, err := app.email.constructReset(pwr, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
||||||
|
|||||||
16
scripts/scrape-custom-content-schema.py
Normal file
16
scripts/scrape-custom-content-schema.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Quick script to scrape custom content names, variables and conditionals. The decision to generate vars and conds dynamically from the included plaintext emails, then bodge extra variables on top was stupid, and is only done -once- before getting stored in the DB indefinitely, meaning new variables can't easily be added. Output of this will be coalesced into a predefined list included with the software.
|
||||||
|
import requests, json
|
||||||
|
|
||||||
|
content = requests.get("http://localhost:8056/config/emails?lang=en-gb&filter=user").json()
|
||||||
|
|
||||||
|
out = {}
|
||||||
|
|
||||||
|
for key in content:
|
||||||
|
resp = requests.get("http://localhost:8056/config/emails/"+key)
|
||||||
|
out[key] = resp.json()
|
||||||
|
del out[key]["html"]
|
||||||
|
del out[key]["plaintext"]
|
||||||
|
del out[key]["content"]
|
||||||
|
|
||||||
|
print(json.dumps(out, indent=4))
|
||||||
|
|
||||||
93
storage.go
93
storage.go
@@ -15,10 +15,8 @@ import (
|
|||||||
"github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
"github.com/hrfee/jfa-go/logger"
|
"github.com/hrfee/jfa-go/logger"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
"gopkg.in/ini.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type discordStore map[string]DiscordUser
|
type discordStore map[string]DiscordUser
|
||||||
@@ -80,7 +78,7 @@ const (
|
|||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
debug *logger.Logger
|
debug *logger.Logger
|
||||||
logActions map[string]DebugLogAction
|
logActions func(k string) DebugLogAction
|
||||||
|
|
||||||
timePattern string
|
timePattern string
|
||||||
|
|
||||||
@@ -104,6 +102,44 @@ type Storage struct {
|
|||||||
lang Lang
|
lang Lang
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStorage returns a new Storage object with values initialised.
|
||||||
|
func NewStorage(dbPath string, debugLogger *logger.Logger, logActions func(k string) DebugLogAction) *Storage {
|
||||||
|
if debugLogger.Empty {
|
||||||
|
debugLogger = nil
|
||||||
|
}
|
||||||
|
st := &Storage{
|
||||||
|
debug: debugLogger,
|
||||||
|
logActions: logActions,
|
||||||
|
db_path: dbPath,
|
||||||
|
}
|
||||||
|
st.lang.CommonPath = "common"
|
||||||
|
st.lang.UserPath = "form"
|
||||||
|
st.lang.AdminPath = "admin"
|
||||||
|
st.lang.EmailPath = "email"
|
||||||
|
st.lang.TelegramPath = "telegram"
|
||||||
|
st.lang.PasswordResetPath = "pwreset"
|
||||||
|
st.lang.SetupPath = "setup"
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect connects to the underlying data storage method (e.g. db).
|
||||||
|
// Call Close() once finished.
|
||||||
|
func (st *Storage) Connect(config *Config) error {
|
||||||
|
opts := badgerhold.DefaultOptions
|
||||||
|
// ValueLogFileSize is in bytes, so multiply by 1e6
|
||||||
|
opts.Options.ValueLogFileSize = config.Section("advanced").Key("value_log_size").MustInt64(256) * 1e6
|
||||||
|
opts.Dir = st.db_path
|
||||||
|
opts.ValueDir = st.db_path
|
||||||
|
var err error = nil
|
||||||
|
st.db, err = badgerhold.Open(opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the underlying data storage method (e.g. db).
|
||||||
|
func (st *Storage) Close() error {
|
||||||
|
return st.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
type StoreType int
|
type StoreType int
|
||||||
|
|
||||||
// Used for debug logging of storage.
|
// Used for debug logging of storage.
|
||||||
@@ -146,7 +182,7 @@ func (st *Storage) DebugWatch(storeType StoreType, key, mainData string) {
|
|||||||
actionKey = "custom_content"
|
actionKey = "custom_content"
|
||||||
}
|
}
|
||||||
|
|
||||||
logAction := st.logActions[actionKey]
|
logAction := st.logActions(actionKey)
|
||||||
if logAction == NoLog {
|
if logAction == NoLog {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,7 +195,7 @@ func (st *Storage) DebugWatch(storeType StoreType, key, mainData string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateLogActions(c *ini.File) map[string]DebugLogAction {
|
func generateLogActions(c *Config) func(k string) DebugLogAction {
|
||||||
m := map[string]DebugLogAction{}
|
m := map[string]DebugLogAction{}
|
||||||
for _, v := range []string{"emails", "discord", "telegram", "matrix", "invites", "announcements", "expirires", "profiles", "custom_content"} {
|
for _, v := range []string{"emails", "discord", "telegram", "matrix", "invites", "announcements", "expirires", "profiles", "custom_content"} {
|
||||||
switch c.Section("advanced").Key("debug_log_" + v).MustString("none") {
|
switch c.Section("advanced").Key("debug_log_" + v).MustString("none") {
|
||||||
@@ -171,21 +207,7 @@ func generateLogActions(c *ini.File) map[string]DebugLogAction {
|
|||||||
m[v] = LogDeletion
|
m[v] = LogDeletion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m
|
return func(k string) DebugLogAction { return m[k] }
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) ConnectDB() {
|
|
||||||
opts := badgerhold.DefaultOptions
|
|
||||||
// ValueLogFileSize is in bytes, so multiply by 1e6
|
|
||||||
opts.Options.ValueLogFileSize = app.config.Section("advanced").Key("value_log_size").MustInt64(256) * 1e6
|
|
||||||
opts.Dir = app.storage.db_path
|
|
||||||
opts.ValueDir = app.storage.db_path
|
|
||||||
db, err := badgerhold.Open(opts)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err)
|
|
||||||
}
|
|
||||||
app.storage.db = db
|
|
||||||
app.info.Printf(lm.ConnectDB, app.storage.db_path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEmails returns a copy of the store.
|
// GetEmails returns a copy of the store.
|
||||||
@@ -683,13 +705,34 @@ type customEmails struct {
|
|||||||
ExpiryReminder CustomContent `json:"expiryReminder"`
|
ExpiryReminder CustomContent `json:"expiryReminder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomContentContext = int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CustomMessage CustomContentContext = iota
|
||||||
|
CustomCard
|
||||||
|
CustomTemplate
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContentSourceFileInfo struct{ Section, SettingPrefix, DefaultValue string }
|
||||||
|
|
||||||
|
// CustomContent stores information needed for creating custom jfa-go content, including emails and user messages.
|
||||||
|
type CustomContentInfo struct {
|
||||||
|
Name string `json:"name" badgerhold:"key"`
|
||||||
|
DisplayName, Description func(dict *Lang, lang string) string
|
||||||
|
Subject func(config *Config, lang *emailLang) string
|
||||||
|
// Config section, the main part of the setting name (without "html" or "text"), and the default filename (without ".html" or ".txt").
|
||||||
|
SourceFile ContentSourceFileInfo
|
||||||
|
ContentType CustomContentContext `json:"type"`
|
||||||
|
Variables []string `json:"variables,omitempty"`
|
||||||
|
Conditionals []string `json:"conditionals,omitempty"`
|
||||||
|
Placeholders map[string]any `json:"values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// CustomContent stores customized versions of jfa-go content, including emails and user messages.
|
// CustomContent stores customized versions of jfa-go content, including emails and user messages.
|
||||||
type CustomContent struct {
|
type CustomContent struct {
|
||||||
Name string `json:"name" badgerhold:"key"`
|
Name string `json:"name" badgerhold:"key"`
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Variables []string `json:"variables,omitempty"`
|
|
||||||
Conditionals []string `json:"conditionals,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type userPageContent struct {
|
type userPageContent struct {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ type Updater struct {
|
|||||||
binary string
|
binary string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
|
func NewUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
|
||||||
// fmt.Printf(`Updater intializing with "%s", "%s", "%s", "%s", "%s", "%s"\n`, buildroneURL, namespace, repo, version, commit, buildType)
|
// fmt.Printf(`Updater intializing with "%s", "%s", "%s", "%s", "%s", "%s"\n`, buildroneURL, namespace, repo, version, commit, buildType)
|
||||||
bType := off
|
bType := off
|
||||||
tag := ""
|
tag := ""
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (app *appContext) checkUsers(remindBeforeExpiry *DayTimerSet) {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msg, err := app.email.constructExpiryReminder(user.Name, expiry.Expiry, app, false)
|
msg, err := app.email.constructExpiryReminder(user.Name, expiry.Expiry, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructExpiryReminderMessage, user.ID, err)
|
app.err.Printf(lm.FailedConstructExpiryReminderMessage, user.ID, err)
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||||
@@ -173,7 +173,7 @@ func (app *appContext) checkUsers(remindBeforeExpiry *DayTimerSet) {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msg, err := app.email.constructUserExpired(app, false)
|
msg, err := app.email.constructUserExpired(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
|
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||||
|
|||||||
2
users.go
2
users.go
@@ -169,7 +169,7 @@ func (app *appContext) WelcomeNewUser(user mediabrowser.User, expiry time.Time)
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := app.email.constructWelcome(user.Name, expiry, app, false)
|
msg, err := app.email.constructWelcome(user.Name, expiry, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
|
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||||
|
|||||||
11
views.go
11
views.go
@@ -88,7 +88,7 @@ func (app *appContext) BasePageTemplateValues(gc *gin.Context, page Page, base g
|
|||||||
|
|
||||||
pages := PagePathsDTO{
|
pages := PagePathsDTO{
|
||||||
PagePaths: PAGES,
|
PagePaths: PAGES,
|
||||||
ExternalURI: app.ExternalURI(gc),
|
ExternalURI: ExternalURI(gc),
|
||||||
TrueBase: PAGES.Base,
|
TrueBase: PAGES.Base,
|
||||||
}
|
}
|
||||||
pages.Base = app.getURLBase(gc)
|
pages.Base = app.getURLBase(gc)
|
||||||
@@ -742,7 +742,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
discord := discordEnabled && app.config.Section("discord").Key("show_on_reg").MustBool(true)
|
discord := discordEnabled && app.config.Section("discord").Key("show_on_reg").MustBool(true)
|
||||||
matrix := matrixEnabled && app.config.Section("matrix").Key("show_on_reg").MustBool(true)
|
matrix := matrixEnabled && app.config.Section("matrix").Key("show_on_reg").MustBool(true)
|
||||||
|
|
||||||
userPageAddress := app.ExternalURI(gc) + PAGES.MyAccount
|
userPageAddress := ExternalURI(gc) + PAGES.MyAccount
|
||||||
|
|
||||||
fromUser := ""
|
fromUser := ""
|
||||||
if invite.ReferrerJellyfinID != "" {
|
if invite.ReferrerJellyfinID != "" {
|
||||||
@@ -810,14 +810,15 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
||||||
}
|
}
|
||||||
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
|
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
|
||||||
|
cci := customContent["PostSignupCard"]
|
||||||
data["customSuccessCard"] = true
|
data["customSuccessCard"] = true
|
||||||
// We don't template here, since the username is only known after login.
|
// We don't template here, since the username is only known after login.
|
||||||
templated, err := templateEmail(
|
templated, err := templateEmail(
|
||||||
msg.Content,
|
msg.Content,
|
||||||
msg.Variables,
|
cci.Variables,
|
||||||
msg.Conditionals,
|
cci.Conditionals,
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"username": "{username}",
|
"username": "{username}", // Value is subbed by webpage
|
||||||
"myAccountURL": userPageAddress,
|
"myAccountURL": userPageAddress,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user