mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-26 19:10:10 +00:00
Compare commits
4 Commits
2a6937228c
...
d60dea61db
Author | SHA1 | Date | |
---|---|---|---|
d60dea61db | |||
a136800ff2 | |||
db1c62cc46 | |||
1fa340f096 |
13
api-users.go
13
api-users.go
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -529,7 +528,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
||||
}
|
||||
if telegramVerified {
|
||||
u, _ := app.storage.GetTelegramKey(user.ID)
|
||||
contactMethods[jellyseerr.FieldTelegram] = strconv.FormatInt(u.ChatID, 10)
|
||||
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
|
||||
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
||||
}
|
||||
if emailEnabled || discordVerified || telegramVerified {
|
||||
@ -1373,7 +1372,9 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
configuration = profile.Configuration
|
||||
displayprefs = profile.Displayprefs
|
||||
}
|
||||
if req.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
|
||||
@ -1395,7 +1396,9 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
applyingFrom = "\"" + user.Name + "\""
|
||||
if req.Policy {
|
||||
policy = user.Policy
|
||||
}
|
||||
if req.Homescreen {
|
||||
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
@ -1422,10 +1425,14 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
app.debug.Println("Adding delay between requests for large batch")
|
||||
}
|
||||
for _, id := range req.ApplyTo {
|
||||
status, err := app.jf.SetPolicy(id, policy)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if shouldDelay {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
15
backups.go
15
backups.go
@ -161,20 +161,13 @@ func (app *appContext) loadPendingBackup() {
|
||||
LOADBAK = ""
|
||||
}
|
||||
|
||||
func newBackupDaemon(app *appContext) *housekeepingDaemon {
|
||||
func newBackupDaemon(app *appContext) *GenericDaemon {
|
||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||
daemon := housekeepingDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
app: app,
|
||||
}
|
||||
daemon.jobs = []func(app *appContext){
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.debug.Println("Backups: Creating backup")
|
||||
app.makeBackup()
|
||||
},
|
||||
}
|
||||
return &daemon
|
||||
)
|
||||
return d
|
||||
}
|
||||
|
@ -1620,6 +1620,23 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
69
daemon.go
69
daemon.go
@ -116,32 +116,16 @@ func (app *appContext) clearActivities() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
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)
|
||||
@ -150,53 +134,24 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||
|
||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||
d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||
}
|
||||
|
||||
if clearEmail {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
|
||||
d.appendJobs(func(app *appContext) { app.clearEmails() })
|
||||
}
|
||||
if clearDiscord {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
|
||||
d.appendJobs(func(app *appContext) { app.clearDiscord() })
|
||||
}
|
||||
if clearTelegram {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
|
||||
d.appendJobs(func(app *appContext) { app.clearTelegram() })
|
||||
}
|
||||
if clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
||||
d.appendJobs(func(app *appContext) { app.clearMatrix() })
|
||||
}
|
||||
if clearPWR {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
|
||||
d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() })
|
||||
}
|
||||
|
||||
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)
|
||||
return d
|
||||
}
|
||||
|
65
genericdaemon.go
Normal file
65
genericdaemon.go
Normal file
@ -0,0 +1,65 @@
|
||||
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)
|
||||
}
|
@ -101,6 +101,10 @@
|
||||
<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>
|
||||
|
@ -30,6 +30,7 @@ type Jellyseerr struct {
|
||||
cacheLength time.Duration
|
||||
timeoutHandler common.TimeoutHandler
|
||||
LogRequestBodies bool
|
||||
AutoImportUsers bool
|
||||
}
|
||||
|
||||
// NewJellyseerr returns an Ombi object.
|
||||
@ -54,73 +55,60 @@ func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Je
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
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
|
||||
}
|
||||
var req *http.Request
|
||||
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))
|
||||
if data != nil {
|
||||
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
||||
} else {
|
||||
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), nil)
|
||||
req, _ = http.NewRequest(mode, uri, 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)
|
||||
}
|
||||
}
|
||||
resp, err := js.httpClient.Do(req)
|
||||
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
|
||||
defer js.timeoutHandler()
|
||||
if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
|
||||
if resp.StatusCode == 401 {
|
||||
return "", 401, fmt.Errorf("Invalid API Key")
|
||||
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)
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
if response {
|
||||
defer resp.Body.Close()
|
||||
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":
|
||||
@ -129,21 +117,28 @@ func (js *Jellyseerr) send(mode string, url string, data any, response bool, hea
|
||||
out = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err = io.Copy(buf, out)
|
||||
_, err := io.Copy(buf, out)
|
||||
if err != nil {
|
||||
return "", 500, err
|
||||
return "", err
|
||||
}
|
||||
responseText = buf.String()
|
||||
}
|
||||
return responseText, resp.StatusCode, nil
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) post(url string, data any, response bool) (string, int, error) {
|
||||
return js.send("POST", url, data, response, 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) put(url string, data any, response bool) (string, int, error) {
|
||||
return js.send("PUT", 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) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
|
||||
@ -175,7 +170,7 @@ func (js *Jellyseerr) getUsers() error {
|
||||
pageCount := 1
|
||||
pageIndex := 0
|
||||
for {
|
||||
res, err := js.getUserPage(0)
|
||||
res, err := js.getUserPage(pageIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -197,9 +192,12 @@ 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))
|
||||
params.Add("skip", strconv.Itoa(page*30))
|
||||
params.Add("sort", "created")
|
||||
resp, status, err := js.getJSON(js.server+"/user", nil, params)
|
||||
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)
|
||||
var data GetUsersDTO
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
@ -211,29 +209,59 @@ 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) {
|
||||
js.getUsers()
|
||||
if u, ok := js.userCache[jfID]; ok {
|
||||
return u, nil
|
||||
}
|
||||
users, err := js.ImportFromJellyfin(jfID)
|
||||
var u User
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
var users []User
|
||||
users, err = js.ImportFromJellyfin(jfID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(users) != 0 {
|
||||
return users[0], err
|
||||
u = users[0]
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
if u, ok := js.userCache[jfID]; ok {
|
||||
return u, nil
|
||||
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
|
||||
}
|
||||
return u, fmt.Errorf("user not found")
|
||||
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)
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) Me() (User, error) {
|
||||
resp, status, err := js.getJSON(js.server+"/auth/me", nil, url.Values{})
|
||||
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
||||
var data User
|
||||
data.ID = -1
|
||||
if status != 200 {
|
||||
@ -248,12 +276,12 @@ func (js *Jellyseerr) Me() (User, error) {
|
||||
|
||||
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
||||
data := permissionsDTO{Permissions: -1}
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return data.Permissions, err
|
||||
}
|
||||
|
||||
resp, status, err := js.getJSON(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||
if err != nil {
|
||||
return data.Permissions, err
|
||||
}
|
||||
@ -265,7 +293,7 @@ func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -283,7 +311,7 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error {
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -304,7 +332,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.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -322,12 +350,12 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) DeleteUser(jfID string) error {
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.send("DELETE", fmt.Sprintf(js.server+"/user/%d", u.ID), nil, false, nil)
|
||||
status, err := js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
@ -339,7 +367,7 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return Notifications{}, err
|
||||
}
|
||||
@ -348,7 +376,7 @@ func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, er
|
||||
|
||||
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
|
||||
var data Notifications
|
||||
resp, status, err := js.getJSON(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@ -364,7 +392,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
|
||||
/* if tmpl.NotifTypes.Empty() {
|
||||
tmpl.NotifTypes = nil
|
||||
}*/
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -380,7 +408,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -401,7 +429,7 @@ func (js *Jellyseerr) GetUsers() (map[string]User, error) {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
||||
resp, status, err := js.getJSON(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||
var data User
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
@ -414,12 +442,12 @@ func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error {
|
||||
u, err := js.MustGetUser(jfID)
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -130,3 +130,7 @@ type MainUserSettings struct {
|
||||
WatchlistSyncMovies any `json:"watchlistSyncMovies,omitempty"`
|
||||
WatchlistSyncTv any `json:"watchlistSyncTv,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorDTO struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
81
jellyseerrdaemon.go
Normal file
81
jellyseerrdaemon.go
Normal file
@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
@ -81,6 +81,7 @@
|
||||
"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
15
main.go
@ -369,6 +369,7 @@ 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
|
||||
|
||||
}
|
||||
@ -480,13 +481,21 @@ func start(asDaemon, firstCall bool) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
||||
invDaemon := newHousekeepingDaemon(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()
|
||||
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()
|
||||
}
|
||||
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
||||
go app.StartPWR()
|
||||
@ -496,7 +505,7 @@ func start(asDaemon, firstCall bool) {
|
||||
go app.checkForUpdates()
|
||||
}
|
||||
|
||||
var backupDaemon *housekeepingDaemon
|
||||
var backupDaemon *GenericDaemon
|
||||
if app.config.Section("backups").Key("enabled").MustBool(false) {
|
||||
backupDaemon = newBackupDaemon(app)
|
||||
go backupDaemon.run()
|
||||
|
@ -178,6 +178,9 @@ type userSettingsDTO struct {
|
||||
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
|
||||
|
@ -795,7 +795,8 @@ export class accountsList {
|
||||
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
|
||||
private _search: Search;
|
||||
|
||||
private _applyHomesreen = document.getElementById("modify-user-homescreen") as HTMLInputElement;
|
||||
private _applyHomescreen = document.getElementById("modify-user-homescreen") as HTMLInputElement;
|
||||
private _applyConfiguration = document.getElementById("modify-user-configuration") as HTMLInputElement;
|
||||
private _applyOmbi = document.getElementById("modify-user-ombi") as HTMLInputElement;
|
||||
private _applyJellyseerr = document.getElementById("modify-user-jellyseerr") as HTMLInputElement;
|
||||
|
||||
@ -1490,7 +1491,8 @@ export class accountsList {
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"apply_to": list,
|
||||
"homescreen": this._applyHomesreen.checked,
|
||||
"homescreen": this._applyHomescreen.checked,
|
||||
"configuration": this._applyConfiguration.checked,
|
||||
"ombi": this._applyOmbi.checked,
|
||||
"jellyseerr": this._applyJellyseerr.checked
|
||||
};
|
||||
|
@ -7,47 +7,14 @@ import (
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
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 newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.checkUsers()
|
||||
},
|
||||
)
|
||||
d.Name("User daemon")
|
||||
return d
|
||||
}
|
||||
|
||||
func (app *appContext) checkUsers() {
|
||||
|
Loading…
Reference in New Issue
Block a user