1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-28 03:50:10 +00:00

Compare commits

..

No commits in common. "d60dea61db0db184b246e068a08ef700b6819f4e" and "2a6937228c34a07cedf51adb376ab2d25046d069" have entirely different histories.

14 changed files with 229 additions and 365 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
@ -528,7 +529,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
}
if telegramVerified {
u, _ := app.storage.GetTelegramKey(user.ID)
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
contactMethods[jellyseerr.FieldTelegram] = strconv.FormatInt(u.ChatID, 10)
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
}
if emailEnabled || discordVerified || telegramVerified {
@ -1372,9 +1373,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
configuration = profile.Configuration
displayprefs = profile.Displayprefs
}
if req.Policy {
policy = profile.Policy
}
policy = profile.Policy
if req.Ombi && app.config.Section("ombi").Key("enabled").MustBool(false) {
if profile.Ombi != nil && len(profile.Ombi) != 0 {
ombi = profile.Ombi
@ -1396,9 +1395,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
return
}
applyingFrom = "\"" + user.Name + "\""
if req.Policy {
policy = user.Policy
}
policy = user.Policy
if req.Homescreen {
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
@ -1425,13 +1422,9 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
app.debug.Println("Adding delay between requests for large batch")
}
for _, id := range req.ApplyTo {
var status int
var err error
if req.Policy {
status, err = app.jf.SetPolicy(id, policy)
if !(status == 200 || status == 204) || err != nil {
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
}
status, err := app.jf.SetPolicy(id, policy)
if !(status == 200 || status == 204) || err != nil {
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
}
if shouldDelay {
time.Sleep(250 * time.Millisecond)

View File

@ -161,13 +161,20 @@ func (app *appContext) loadPendingBackup() {
LOADBAK = ""
}
func newBackupDaemon(app *appContext) *GenericDaemon {
func newBackupDaemon(app *appContext) *housekeepingDaemon {
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
d := NewGenericDaemon(interval, app,
daemon := housekeepingDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
daemon.jobs = []func(app *appContext){
func(app *appContext) {
app.debug.Println("Backups: Creating backup")
app.makeBackup()
},
)
return d
}
return &daemon
}

View File

@ -1620,23 +1620,6 @@
"value": "",
"depends_true": "enabled",
"description": "API Key. Get this from the first tab in Jellyseerr's settings."
},
"import_existing": {
"name": "Import existing users to Jellyseerr",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"depends_true": "enabled",
"description": "Existing users (and those created outside jfa-go) will have their contact info imported to Jellyseerr."
},
"constraints_note": {
"name": "Unique Emails:",
"type": "note",
"value": "",
"depends_true": "import_existing",
"required": "false",
"description": "Jellyseerr requires email addresses to be unique. If this is not the case, you may see errors in jfa-go's logs. You can require unique addresses in Settings > Email."
}
}
},

View File

@ -116,16 +116,32 @@ func (app *appContext) clearActivities() {
}
}
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
type housekeepingDaemon struct {
Stopped bool
ShutdownChannel chan string
Interval time.Duration
period time.Duration
jobs []func(app *appContext)
app *appContext
}
func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon {
daemon := housekeepingDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
daemon.jobs = []func(app *appContext){
func(app *appContext) {
app.debug.Println("Housekeeping: Checking for expired invites")
app.checkInvites()
},
func(app *appContext) { app.clearActivities() },
)
d.Name("Housekeeping daemon")
}
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
@ -134,24 +150,53 @@ func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaem
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
}
if clearEmail {
d.appendJobs(func(app *appContext) { app.clearEmails() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
}
if clearDiscord {
d.appendJobs(func(app *appContext) { app.clearDiscord() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
}
if clearTelegram {
d.appendJobs(func(app *appContext) { app.clearTelegram() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
}
if clearMatrix {
d.appendJobs(func(app *appContext) { app.clearMatrix() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
}
if clearPWR {
d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
}
return d
return &daemon
}
func (rt *housekeepingDaemon) run() {
rt.app.info.Println("Invite daemon started")
for {
select {
case <-rt.ShutdownChannel:
rt.ShutdownChannel <- "Down"
return
case <-time.After(rt.period):
break
}
started := time.Now()
for _, job := range rt.jobs {
job(rt.app)
}
finished := time.Now()
duration := finished.Sub(started)
rt.period = rt.Interval - duration
}
}
func (rt *housekeepingDaemon) Shutdown() {
rt.Stopped = true
rt.ShutdownChannel <- "Down"
<-rt.ShutdownChannel
close(rt.ShutdownChannel)
}

View File

@ -1,65 +0,0 @@
package main
import "time"
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
type GenericDaemon struct {
Stopped bool
ShutdownChannel chan string
Interval time.Duration
period time.Duration
jobs []func(app *appContext)
app *appContext
name string
}
func (d *GenericDaemon) appendJobs(jobs ...func(app *appContext)) {
d.jobs = append(d.jobs, jobs...)
}
// NewGenericDaemon returns a daemon which can be given jobs that utilize appContext.
func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app *appContext)) *GenericDaemon {
d := GenericDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
name: "Generic Daemon",
}
d.jobs = jobs
return &d
}
func (d *GenericDaemon) Name(name string) { d.name = name }
func (d *GenericDaemon) run() {
d.app.info.Printf("%s started", d.name)
for {
select {
case <-d.ShutdownChannel:
d.ShutdownChannel <- "Down"
return
case <-time.After(d.period):
break
}
started := time.Now()
for _, job := range d.jobs {
job(d.app)
}
finished := time.Now()
duration := finished.Sub(started)
d.period = d.Interval - duration
}
}
func (d *GenericDaemon) Shutdown() {
d.Stopped = true
d.ShutdownChannel <- "Down"
<-d.ShutdownChannel
close(d.ShutdownChannel)
}

View File

@ -101,10 +101,6 @@
<div class="select ~neutral @low unfocused">
<select id="modify-user-users"></select>
</div>
<label class="switch">
<input type="checkbox" id="modify-user-configuration" checked>
<span>{{ .strings.applyConfigurationAndPolicy }}</span>
</label>
<label class="switch">
<input type="checkbox" id="modify-user-homescreen" checked>
<span>{{ .strings.applyHomescreenLayout }}</span>

View File

@ -30,7 +30,6 @@ type Jellyseerr struct {
cacheLength time.Duration
timeoutHandler common.TimeoutHandler
LogRequestBodies bool
AutoImportUsers bool
}
// NewJellyseerr returns an Ombi object.
@ -55,90 +54,96 @@ func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Je
}
}
func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
var params []byte
if data != nil {
params, _ = json.Marshal(data)
}
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\" to \"%s\"\n", string(params), uri)
}
if qp := queryParams.Encode(); qp != "" {
uri += "?" + qp
// does a GET and returns the response as a string.
func (js *Jellyseerr) getJSON(url string, params map[string]string, queryParams url.Values) (string, int, error) {
if js.key == "" {
return "", 401, fmt.Errorf("No API key provided")
}
var req *http.Request
if data != nil {
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
if params != nil {
jsonParams, _ := json.Marshal(params)
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\"\n", string(jsonParams))
}
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), bytes.NewBuffer(jsonParams))
} else {
req, _ = http.NewRequest(mode, uri, nil)
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), nil)
}
for name, value := range js.header {
req.Header.Add(name, value)
}
resp, err := js.httpClient.Do(req)
defer js.timeoutHandler()
if err != nil || resp.StatusCode != 200 {
if resp.StatusCode == 401 {
return "", 401, fmt.Errorf("Invalid API Key")
}
return "", resp.StatusCode, err
}
defer resp.Body.Close()
var data io.Reader
switch resp.Header.Get("Content-Encoding") {
case "gzip":
data, _ = gzip.NewReader(resp.Body)
default:
data = resp.Body
}
buf := new(strings.Builder)
_, err = io.Copy(buf, data)
if err != nil {
return "", 500, err
}
return buf.String(), resp.StatusCode, nil
}
// does a POST and optionally returns response as string. Returns a string instead of an io.reader bcs i couldn't get it working otherwise.
func (js *Jellyseerr) send(mode string, url string, data any, response bool, headers map[string]string) (string, int, error) {
responseText := ""
params, _ := json.Marshal(data)
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\"\n", string(params))
}
req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params))
req.Header.Add("Content-Type", "application/json")
for name, value := range js.header {
req.Header.Add(name, value)
}
if headers != nil {
for name, value := range headers {
req.Header.Add(name, value)
}
for name, value := range headers {
req.Header.Add(name, value)
}
resp, err := js.httpClient.Do(req)
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
defer js.timeoutHandler()
var responseText string
defer resp.Body.Close()
if response || reqFailed {
responseText, err = js.decodeResp(resp)
if err != nil {
return responseText, resp.StatusCode, err
}
}
if reqFailed {
var msg ErrorDTO
err = json.Unmarshal([]byte(responseText), &msg)
if err != nil {
return responseText, resp.StatusCode, err
}
if msg.Message == "" {
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
} else {
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
if resp.StatusCode == 401 {
return "", 401, fmt.Errorf("Invalid API Key")
}
return responseText, resp.StatusCode, err
}
return responseText, resp.StatusCode, err
}
func (js *Jellyseerr) decodeResp(resp *http.Response) (string, error) {
var out io.Reader
switch resp.Header.Get("Content-Encoding") {
case "gzip":
out, _ = gzip.NewReader(resp.Body)
default:
out = resp.Body
if response {
defer resp.Body.Close()
var out io.Reader
switch resp.Header.Get("Content-Encoding") {
case "gzip":
out, _ = gzip.NewReader(resp.Body)
default:
out = resp.Body
}
buf := new(strings.Builder)
_, err = io.Copy(buf, out)
if err != nil {
return "", 500, err
}
responseText = buf.String()
}
buf := new(strings.Builder)
_, err := io.Copy(buf, out)
if err != nil {
return "", err
}
return buf.String(), nil
return responseText, resp.StatusCode, nil
}
func (js *Jellyseerr) get(uri string, data any, params url.Values) (string, int, error) {
return js.req(http.MethodGet, uri, data, params, nil, true)
func (js *Jellyseerr) post(url string, data any, response bool) (string, int, error) {
return js.send("POST", url, data, response, nil)
}
func (js *Jellyseerr) post(uri string, data any, response bool) (string, int, error) {
return js.req(http.MethodPost, uri, data, url.Values{}, nil, response)
}
func (js *Jellyseerr) put(uri string, data any, response bool) (string, int, error) {
return js.req(http.MethodPut, uri, data, url.Values{}, nil, response)
}
func (js *Jellyseerr) delete(uri string, data any) (int, error) {
_, status, err := js.req(http.MethodDelete, uri, data, url.Values{}, nil, false)
return status, err
func (js *Jellyseerr) put(url string, data any, response bool) (string, int, error) {
return js.send("PUT", url, data, response, nil)
}
func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
@ -170,7 +175,7 @@ func (js *Jellyseerr) getUsers() error {
pageCount := 1
pageIndex := 0
for {
res, err := js.getUserPage(pageIndex)
res, err := js.getUserPage(0)
if err != nil {
return err
}
@ -192,12 +197,9 @@ func (js *Jellyseerr) getUsers() error {
func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
params := url.Values{}
params.Add("take", "30")
params.Add("skip", strconv.Itoa(page*30))
params.Add("skip", strconv.Itoa(page))
params.Add("sort", "created")
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
}
resp, status, err := js.get(js.server+"/user", nil, params)
resp, status, err := js.getJSON(js.server+"/user", nil, params)
var data GetUsersDTO
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
@ -209,59 +211,29 @@ func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
return data, err
}
// MustGetUser provides the same function as ImportFromJellyfin, but will always return the user,
// even if they already existed.
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
u, _, err := js.GetOrImportUser(jfID)
return u, err
}
// GetImportedUser provides the same function as ImportFromJellyfin, but will always return the user,
// even if they already existed. Also returns whether the user was imported or not,
func (js *Jellyseerr) GetOrImportUser(jfID string) (u User, imported bool, err error) {
imported = false
u, err = js.GetExistingUser(jfID)
if err == nil {
return
js.getUsers()
if u, ok := js.userCache[jfID]; ok {
return u, nil
}
var users []User
users, err = js.ImportFromJellyfin(jfID)
users, err := js.ImportFromJellyfin(jfID)
var u User
if err != nil {
return
return u, err
}
if len(users) != 0 {
u = users[0]
err = nil
return
return users[0], err
}
err = fmt.Errorf("user not found or imported")
return
}
func (js *Jellyseerr) GetExistingUser(jfID string) (u User, err error) {
js.getUsers()
ok := false
err = nil
if u, ok = js.userCache[jfID]; ok {
return
if u, ok := js.userCache[jfID]; ok {
return u, nil
}
js.cacheExpiry = time.Now()
js.getUsers()
if u, ok = js.userCache[jfID]; ok {
err = nil
return
}
err = fmt.Errorf("user not found")
return
}
func (js *Jellyseerr) getUser(jfID string) (User, error) {
if js.AutoImportUsers {
return js.MustGetUser(jfID)
}
return js.GetExistingUser(jfID)
return u, fmt.Errorf("user not found")
}
func (js *Jellyseerr) Me() (User, error) {
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
resp, status, err := js.getJSON(js.server+"/auth/me", nil, url.Values{})
var data User
data.ID = -1
if status != 200 {
@ -276,12 +248,12 @@ func (js *Jellyseerr) Me() (User, error) {
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
data := permissionsDTO{Permissions: -1}
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return data.Permissions, err
}
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
resp, status, err := js.getJSON(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
if err != nil {
return data.Permissions, err
}
@ -293,7 +265,7 @@ func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
}
func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
@ -311,7 +283,7 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
}
func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error {
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
@ -332,7 +304,7 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
if _, ok := conf[FieldEmail]; ok {
return fmt.Errorf("email is read only, set with ModifyMainUserSettings instead")
}
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
@ -350,12 +322,12 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
}
func (js *Jellyseerr) DeleteUser(jfID string) error {
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
status, err := js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
_, status, err := js.send("DELETE", fmt.Sprintf(js.server+"/user/%d", u.ID), nil, false, nil)
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
@ -367,7 +339,7 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
}
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return Notifications{}, err
}
@ -376,7 +348,7 @@ func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, er
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
var data Notifications
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
resp, status, err := js.getJSON(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
if err != nil {
return data, err
}
@ -392,7 +364,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
/* if tmpl.NotifTypes.Empty() {
tmpl.NotifTypes = nil
}*/
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
@ -408,7 +380,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
}
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
@ -429,7 +401,7 @@ func (js *Jellyseerr) GetUsers() (map[string]User, error) {
}
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
resp, status, err := js.getJSON(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
var data User
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
@ -442,12 +414,12 @@ func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
}
func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error {
u, err := js.getUser(jfID)
u, err := js.MustGetUser(jfID)
if err != nil {
return err
}
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
if err != nil {
return err
}

View File

@ -130,7 +130,3 @@ type MainUserSettings struct {
WatchlistSyncMovies any `json:"watchlistSyncMovies,omitempty"`
WatchlistSyncTv any `json:"watchlistSyncTv,omitempty"`
}
type ErrorDTO struct {
Message string `json:"message,omitempty"`
}

View File

@ -1,81 +0,0 @@
package main
import (
"strconv"
"time"
"github.com/hrfee/jfa-go/jellyseerr"
)
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
user, imported, err := app.js.GetOrImportUser(jfID)
if err != nil {
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
return
}
if imported {
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
}
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
if err != nil {
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
return
}
contactMethods := map[jellyseerr.NotificationsField]any{}
email, ok := app.storage.GetEmailsKey(jfID)
if ok && email.Addr != "" && user.Email != email.Addr {
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
if err != nil {
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
} else {
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
}
}
if discordEnabled {
dcUser, ok := app.storage.GetDiscordKey(jfID)
if ok && dcUser.ID != "" && notif.DiscordID != dcUser.ID {
contactMethods[jellyseerr.FieldDiscord] = dcUser.ID
contactMethods[jellyseerr.FieldDiscordEnabled] = dcUser.Contact
}
}
if telegramEnabled {
tgUser, ok := app.storage.GetTelegramKey(jfID)
chatID, _ := strconv.ParseInt(notif.TelegramChatID, 10, 64)
if ok && tgUser.ChatID != 0 && chatID != tgUser.ChatID {
u, _ := app.storage.GetTelegramKey(jfID)
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
contactMethods[jellyseerr.FieldTelegramEnabled] = tgUser.Contact
}
}
if len(contactMethods) != 0 {
err := app.js.ModifyNotifications(jfID, contactMethods)
if err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
}
}
func (app *appContext) SynchronizeJellyseerrUsers() {
users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 {
app.err.Printf("Failed to get users (%d): %s", status, err)
return
}
// I'm sure Jellyseerr can handle it,
// but past issues with the Jellyfin db scare me from
// running these concurrently. W/e, its a bg task anyway.
for _, user := range users {
app.SynchronizeJellyseerrUser(user.ID)
}
}
func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
func(app *appContext) {
app.SynchronizeJellyseerrUsers()
},
)
d.Name("Jellyseerr import daemon")
return d
}

View File

@ -81,7 +81,6 @@
"useInviteExpiry": "Set expiry from profile/invite",
"useInviteExpiryNote": "By default, invites expire after 90 days but can be renewed by the user. Enable for the referral to be disabled after the time set.",
"applyHomescreenLayout": "Apply homescreen layout",
"applyConfigurationAndPolicy": "Apply Jellyfin configuration/policy",
"applyOmbi": "Apply Ombi profile (if available)",
"applyJellyseerr": "Apply Jellyseerr profile (if available)",
"sendDeleteNotificationEmail": "Send notification message",

15
main.go
View File

@ -369,7 +369,6 @@ func start(asDaemon, firstCall bool) {
app.config.Section("jellyseerr").Key("api_key").String(),
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
)
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
// app.js.LogRequestBodies = true
}
@ -481,21 +480,13 @@ func start(asDaemon, firstCall bool) {
os.Exit(0)
}
invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app)
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
go invDaemon.run()
defer invDaemon.Shutdown()
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
go userDaemon.run()
defer userDaemon.Shutdown()
var jellyseerrDaemon *GenericDaemon
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {
// jellyseerrDaemon = newJellyseerrDaemon(time.Duration(30*time.Second), app)
jellyseerrDaemon = newJellyseerrDaemon(time.Duration(10*time.Minute), app)
go jellyseerrDaemon.run()
defer jellyseerrDaemon.Shutdown()
}
defer userDaemon.shutdown()
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
go app.StartPWR()
@ -505,7 +496,7 @@ func start(asDaemon, firstCall bool) {
go app.checkForUpdates()
}
var backupDaemon *GenericDaemon
var backupDaemon *housekeepingDaemon
if app.config.Section("backups").Key("enabled").MustBool(false) {
backupDaemon = newBackupDaemon(app)
go backupDaemon.run()

View File

@ -174,16 +174,13 @@ type ombiUsersDTO struct {
type modifyEmailsDTO map[string]string
type userSettingsDTO struct {
From string `json:"from"` // Whether to apply from "user" or "profile"
Profile string `json:"profile"` // Name of profile (if from = "profile")
ApplyTo []string `json:"apply_to"` // Users to apply settings to
ID string `json:"id"` // ID of user (if from = "user")
// Note confusing name: "Configuration" on the admin UI just means it in the sense
// of the account's settings.
Policy bool `json:"configuration"` // Whether to apply jf policy not
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
Ombi bool `json:"ombi"` // Whether to apply ombi profile or not
Jellyseerr bool `json:"jellyseerr"` // Whether to apply jellyseerr profile or not
From string `json:"from"` // Whether to apply from "user" or "profile"
Profile string `json:"profile"` // Name of profile (if from = "profile")
ApplyTo []string `json:"apply_to"` // Users to apply settings to
ID string `json:"id"` // ID of user (if from = "user")
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
Ombi bool `json:"ombi"` // Whether to apply ombi profile or not
Jellyseerr bool `json:"jellyseerr"` // Whether to apply jellyseerr profile or not
}
type announcementDTO struct {

View File

@ -795,8 +795,7 @@ export class accountsList {
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
private _search: Search;
private _applyHomescreen = document.getElementById("modify-user-homescreen") as HTMLInputElement;
private _applyConfiguration = document.getElementById("modify-user-configuration") as HTMLInputElement;
private _applyHomesreen = document.getElementById("modify-user-homescreen") as HTMLInputElement;
private _applyOmbi = document.getElementById("modify-user-ombi") as HTMLInputElement;
private _applyJellyseerr = document.getElementById("modify-user-jellyseerr") as HTMLInputElement;
@ -1491,8 +1490,7 @@ export class accountsList {
toggleLoader(button);
let send = {
"apply_to": list,
"homescreen": this._applyHomescreen.checked,
"configuration": this._applyConfiguration.checked,
"homescreen": this._applyHomesreen.checked,
"ombi": this._applyOmbi.checked,
"jellyseerr": this._applyJellyseerr.checked
};

View File

@ -7,14 +7,47 @@ import (
"github.com/lithammer/shortuuid/v3"
)
func newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
func(app *appContext) {
app.checkUsers()
},
)
d.Name("User daemon")
return d
type userDaemon struct {
Stopped bool
ShutdownChannel chan string
Interval time.Duration
period time.Duration
app *appContext
}
func newUserDaemon(interval time.Duration, app *appContext) *userDaemon {
return &userDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
}
func (rt *userDaemon) run() {
rt.app.info.Println("User daemon started")
for {
select {
case <-rt.ShutdownChannel:
rt.ShutdownChannel <- "Down"
return
case <-time.After(rt.period):
break
}
started := time.Now()
rt.app.checkUsers()
finished := time.Now()
duration := finished.Sub(started)
rt.period = rt.Interval - duration
}
}
func (rt *userDaemon) shutdown() {
rt.Stopped = true
rt.ShutdownChannel <- "Down"
<-rt.ShutdownChannel
close(rt.ShutdownChannel)
}
func (app *appContext) checkUsers() {