package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/knz/strtime" "github.com/lithammer/shortuuid/v3" "gopkg.in/ini.v1" "os" "os/signal" "syscall" "time" ) func (ctx *appContext) loadStrftime() { ctx.datePattern = ctx.config.Section("email").Key("date_format").String() ctx.timePattern = `%H:%M` if val, _ := ctx.config.Section("email").Key("use_24h").Bool(); !val { ctx.timePattern = `%I:%M %p` } return } func (ctx *appContext) prettyTime(dt time.Time) (date, time string) { date, _ = strtime.Strftime(dt, ctx.datePattern) time, _ = strtime.Strftime(dt, ctx.timePattern) return } func (ctx *appContext) formatDatetime(dt time.Time) string { d, t := ctx.prettyTime(dt) return d + " " + t } // https://stackoverflow.com/questions/36530251/time-since-with-months-and-years/36531443#36531443 THANKS func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) { if a.Location() != b.Location() { b = b.In(a.Location()) } if a.After(b) { a, b = b, a } y1, M1, d1 := a.Date() y2, M2, d2 := b.Date() h1, m1, s1 := a.Clock() h2, m2, s2 := b.Clock() year = int(y2 - y1) month = int(M2 - M1) day = int(d2 - d1) hour = int(h2 - h1) min = int(m2 - m1) sec = int(s2 - s1) // Normalize negative values if sec < 0 { sec += 60 min-- } if min < 0 { min += 60 hour-- } if hour < 0 { hour += 24 day-- } if day < 0 { // days in month: t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC) day += 32 - t.Day() month-- } if month < 0 { month += 12 year-- } return } func (ctx *appContext) checkInvites() { current_time := time.Now() ctx.storage.loadInvites() changed := false for code, data := range ctx.storage.invites { expiry := data.ValidTill if current_time.After(expiry) { ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := data.Notify if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { ctx.debug.Printf("%s: Expiry notification", code) for address, settings := range notify { if settings["notify-expiry"] { go func() { if ctx.email.constructExpiry(code, data, ctx) != nil { ctx.err.Printf("%s: Failed to construct expiry notification", code) } else if ctx.email.send(address, ctx) != nil { ctx.err.Printf("%s: Failed to send expiry notification", code) } else { ctx.info.Printf("Sent expiry notification to %s", address) } }() } } } changed = true delete(ctx.storage.invites, code) } } if changed { ctx.storage.storeInvites() } } func (ctx *appContext) checkInvite(code string, used bool, username string) bool { current_time := time.Now() ctx.storage.loadInvites() changed := false if inv, match := ctx.storage.invites[code]; match { expiry := inv.ValidTill if current_time.After(expiry) { ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := inv.Notify if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { ctx.debug.Printf("%s: Expiry notification", code) for address, settings := range notify { if settings["notify-expiry"] { go func() { if ctx.email.constructExpiry(code, inv, ctx) != nil { ctx.err.Printf("%s: Failed to construct expiry notification", code) } else if ctx.email.send(address, ctx) != nil { ctx.err.Printf("%s: Failed to send expiry notification", code) } else { ctx.info.Printf("Sent expiry notification to %s", address) } }() } } } changed = true match = false delete(ctx.storage.invites, code) } else if used { changed = true del := false newInv := inv if newInv.RemainingUses == 1 { del = true delete(ctx.storage.invites, code) } else if newInv.RemainingUses != 0 { // 0 means infinite i guess? newInv.RemainingUses -= 1 } newInv.UsedBy = append(newInv.UsedBy, []string{username, ctx.formatDatetime(current_time)}) if !del { ctx.storage.invites[code] = newInv } } if changed { ctx.storage.storeInvites() } return match } return false } // Routes from now on! type newUserReq struct { Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` Code string `json:"code"` } func (ctx *appContext) NewUser(gc *gin.Context) { var req newUserReq gc.BindJSON(&req) ctx.debug.Printf("%s: New user attempt", req.Code) if !ctx.checkInvite(req.Code, false, "") { ctx.info.Printf("%s New user failed: invalid code", req.Code) gc.JSON(401, map[string]bool{"success": false}) gc.Abort() return } validation := ctx.validator.validate(req.Password) valid := true for _, val := range validation { if !val { valid = false } } if !valid { // 200 bcs idk what i did in js ctx.info.Printf("%s New user failed: Invalid password", req.Code) gc.JSON(200, validation) gc.Abort() return } existingUser, _, _ := ctx.jf.userByName(req.Username, false) if existingUser != nil { msg := fmt.Sprintf("User already exists named %s", req.Username) ctx.info.Printf("%s New user failed: %s", req.Code, msg) respond(401, msg, gc) return } user, status, err := ctx.jf.newUser(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { ctx.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status) respond(401, "Unknown error", gc) return } ctx.checkInvite(req.Code, true, req.Username) invite := ctx.storage.invites[req.Code] if ctx.config.Section("notifications").Key("enabled").MustBool(false) { for address, settings := range invite.Notify { if settings["notify-creation"] { go func() { if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil { ctx.err.Printf("%s: Failed to construct user creation notification", req.Code) ctx.debug.Printf("%s: Error: %s", req.Code, err) } else if ctx.email.send(address, ctx) != nil { ctx.err.Printf("%s: Failed to send user creation notification", req.Code) ctx.debug.Printf("%s: Error: %s", req.Code, err) } else { ctx.info.Printf("%s: Sent user creation notification to %s", req.Code, address) } }() } } } var id string if user["Id"] != nil { id = user["Id"].(string) } if len(ctx.storage.policy) != 0 { status, err = ctx.jf.setPolicy(id, ctx.storage.policy) if !(status == 200 || status == 204) { ctx.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) } } if len(ctx.storage.configuration) != 0 && len(ctx.storage.displayprefs) != 0 { status, err = ctx.jf.setConfiguration(id, ctx.storage.configuration) if (status == 200 || status == 204) && err == nil { status, err = ctx.jf.setDisplayPreferences(id, ctx.storage.displayprefs) } else { ctx.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) } } if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { ctx.storage.emails[id] = req.Email ctx.storage.storeEmails() } gc.JSON(200, validation) } type generateInviteReq struct { Days int `json:"days"` Hours int `json:"hours"` Minutes int `json:"minutes"` Email string `json:"email"` MultipleUses bool `json:"multiple-uses"` NoLimit bool `json:"no-limit"` RemainingUses int `json:"remaining-uses"` } func (ctx *appContext) GenerateInvite(gc *gin.Context) { var req generateInviteReq ctx.debug.Println("Generating new invite") ctx.storage.loadInvites() gc.BindJSON(&req) current_time := time.Now() valid_till := current_time.AddDate(0, 0, req.Days) valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) invite_code := shortuuid.New() var invite Invite invite.Created = current_time if req.MultipleUses { if req.NoLimit { invite.NoLimit = true } else { invite.RemainingUses = req.RemainingUses } } else { invite.RemainingUses = 1 } invite.ValidTill = valid_till if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) { ctx.debug.Printf("%s: Sending invite email", invite_code) invite.Email = req.Email if err := ctx.email.constructInvite(invite_code, invite, ctx); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) ctx.err.Printf("%s: Failed to construct invite email", invite_code) ctx.debug.Printf("%s: Error: %s", invite_code, err) } else if err := ctx.email.send(req.Email, ctx); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) ctx.err.Printf("%s: %s", invite_code, invite.Email) ctx.debug.Printf("%s: Error: %s", invite_code, err) } else { ctx.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) } } ctx.storage.invites[invite_code] = invite ctx.storage.storeInvites() gc.JSON(200, map[string]bool{"success": true}) } func (ctx *appContext) GetInvites(gc *gin.Context) { ctx.debug.Println("Invites requested") current_time := time.Now() ctx.storage.loadInvites() ctx.checkInvites() var invites []map[string]interface{} for code, inv := range ctx.storage.invites { _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time) invite := make(map[string]interface{}) invite["code"] = code invite["days"] = days invite["hours"] = hours invite["minutes"] = minutes invite["created"] = ctx.formatDatetime(inv.Created) if len(inv.UsedBy) != 0 { invite["used-by"] = inv.UsedBy } if inv.NoLimit { invite["no-limit"] = true } invite["remaining-uses"] = 1 if inv.RemainingUses != 0 { invite["remaining-uses"] = inv.RemainingUses } if inv.Email != "" { invite["email"] = inv.Email } if len(inv.Notify) != 0 { var address string if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { ctx.storage.loadEmails() address = ctx.storage.emails[gc.GetString("jfId")].(string) } else { address = ctx.config.Section("ui").Key("email").String() } if _, ok := inv.Notify[address]; ok { for _, notify_type := range []string{"notify-expiry", "notify-creation"} { if _, ok = inv.Notify[notify_type]; ok { invite[notify_type] = inv.Notify[address][notify_type] } } } } invites = append(invites, invite) } resp := map[string][]map[string]interface{}{ "invites": invites, } gc.JSON(200, resp) } type notifySetting struct { NotifyExpiry bool `json:"notify-expiry"` NotifyCreation bool `json:"notify-creation"` } func (ctx *appContext) SetNotify(gc *gin.Context) { var req map[string]notifySetting gc.BindJSON(&req) changed := false for code, settings := range req { ctx.debug.Printf("%s: Notification settings change requested", code) ctx.storage.loadInvites() ctx.storage.loadEmails() invite, ok := ctx.storage.invites[code] if !ok { ctx.err.Printf("%s Notification setting change failed: Invalid code", code) gc.JSON(400, map[string]string{"error": "Invalid invite code"}) gc.Abort() return } var address string if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { var ok bool address, ok = ctx.storage.emails[gc.GetString("jfId")].(string) if !ok { ctx.err.Printf("%s: Couldn't find email address. Make sure it's set", code) ctx.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) gc.JSON(500, map[string]string{"error": "Missing user email"}) gc.Abort() return } } else { address = ctx.config.Section("ui").Key("email").String() } if invite.Notify == nil { invite.Notify = map[string]map[string]bool{} } if _, ok := invite.Notify[address]; !ok { invite.Notify[address] = map[string]bool{} } /*else { if _, ok := invite.Notify[address]["notify-expiry"]; !ok { */ if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry { invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry ctx.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address) changed = true } if invite.Notify[address]["notify-creation"] != settings.NotifyCreation { invite.Notify[address]["notify-creation"] = settings.NotifyCreation ctx.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address) changed = true } if changed { ctx.storage.invites[code] = invite } } if changed { ctx.storage.storeInvites() } } type deleteReq struct { Code string `json:"code"` } func (ctx *appContext) DeleteInvite(gc *gin.Context) { var req deleteReq gc.BindJSON(&req) ctx.debug.Printf("%s: Deletion requested", req.Code) var ok bool _, ok = ctx.storage.invites[req.Code] if ok { delete(ctx.storage.invites, req.Code) ctx.storage.storeInvites() ctx.info.Printf("%s: Invite deleted", req.Code) gc.JSON(200, map[string]bool{"success": true}) return } ctx.err.Printf("%s: Deletion failed: Invalid code", req.Code) respond(401, "Code doesn't exist", gc) } type userResp struct { UserList []respUser `json:"users"` } type respUser struct { Name string `json:"name"` Email string `json:"email,omitempty"` } func (ctx *appContext) GetUsers(gc *gin.Context) { ctx.debug.Println("Users requested") var resp userResp resp.UserList = []respUser{} users, status, err := ctx.jf.getUsers(false) if !(status == 200 || status == 204) || err != nil { ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) ctx.debug.Printf("Error: %s", err) respond(500, "Couldn't get users", gc) return } for _, jfUser := range users { var user respUser user.Name = jfUser["Name"].(string) if email, ok := ctx.storage.emails[jfUser["Id"].(string)]; ok { user.Email = email.(string) } resp.UserList = append(resp.UserList, user) } gc.JSON(200, resp) } func (ctx *appContext) ModifyEmails(gc *gin.Context) { var req map[string]string gc.BindJSON(&req) ctx.debug.Println("Email modification requested") users, status, err := ctx.jf.getUsers(false) if !(status == 200 || status == 204) || err != nil { ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) ctx.debug.Printf("Error: %s", err) respond(500, "Couldn't get users", gc) return } for _, jfUser := range users { if address, ok := req[jfUser["Name"].(string)]; ok { ctx.storage.emails[jfUser["Id"].(string)] = address } } ctx.storage.storeEmails() ctx.info.Println("Email list modified") gc.JSON(200, map[string]bool{"success": true}) } type defaultsReq struct { Username string `json:"username"` Homescreen bool `json:"homescreen"` } func (ctx *appContext) SetDefaults(gc *gin.Context) { var req defaultsReq gc.BindJSON(&req) ctx.info.Printf("Getting user defaults from \"%s\"", req.Username) user, status, err := ctx.jf.userByName(req.Username, false) if !(status == 200 || status == 204) || err != nil { ctx.err.Printf("Failed to get user from Jellyfin: Code %d", status) ctx.debug.Printf("Error: %s", err) respond(500, "Couldn't get user", gc) return } userId := user["Id"].(string) policy := user["Policy"].(map[string]interface{}) ctx.storage.policy = policy ctx.storage.storePolicy() ctx.debug.Println("User policy template stored") if req.Homescreen { configuration := user["Configuration"].(map[string]interface{}) var displayprefs map[string]interface{} displayprefs, status, err = ctx.jf.getDisplayPreferences(userId) if !(status == 200 || status == 204) || err != nil { ctx.err.Printf("Failed to get DisplayPrefs: Code %d", status) ctx.debug.Printf("Error: %s", err) respond(500, "Couldn't get displayprefs", gc) return } ctx.storage.configuration = configuration ctx.storage.displayprefs = displayprefs ctx.storage.storeConfiguration() ctx.debug.Println("Configuration template stored") ctx.storage.storeDisplayprefs() ctx.debug.Println("DisplayPrefs template stored") } gc.JSON(200, map[string]bool{"success": true}) } func (ctx *appContext) GetConfig(gc *gin.Context) { ctx.info.Println("Config requested") resp := map[string]interface{}{} for section, settings := range ctx.configBase { if section == "order" { resp[section] = settings.([]interface{}) } else { resp[section] = make(map[string]interface{}) for key, values := range settings.(map[string]interface{}) { if key == "order" { resp[section].(map[string]interface{})[key] = values.([]interface{}) } else { resp[section].(map[string]interface{})[key] = values.(map[string]interface{}) if key != "meta" { dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string) configKey := ctx.config.Section(section).Key(key) if dataType == "number" { if val, err := configKey.Int(); err == nil { resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val } } else if dataType == "bool" { resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.MustBool(false) } else { resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.String() } } } } } } gc.JSON(200, resp) } func (ctx *appContext) ModifyConfig(gc *gin.Context) { ctx.info.Println("Config modification requested") var req map[string]interface{} gc.BindJSON(&req) tempConfig, _ := ini.Load(ctx.config_path) for section, settings := range req { _, err := tempConfig.GetSection(section) if section != "restart-program" && err == nil { for setting, value := range settings.(map[string]interface{}) { tempConfig.Section(section).Key(setting).SetValue(value.(string)) } } } tempConfig.SaveTo(ctx.config_path) ctx.debug.Println("Config saved") gc.JSON(200, map[string]bool{"success": true}) if req["restart-program"].(bool) { ctx.info.Println("Restarting...") err := ctx.Restart() if err != nil { ctx.err.Printf("Couldn't restart, try restarting manually. (%s)", err) } } ctx.loadConfig() // Reinitialize password validator on config change, as opposed to every applicable request like in python. if _, ok := req["password_validation"]; ok { ctx.debug.Println("Reinitializing validator") validatorConf := ValidatorConf{ "characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0), "uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0), "lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0), "numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), "special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), } if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { for key := range validatorConf { validatorConf[key] = 0 } } ctx.validator.init(validatorConf) } } // func Restart() error { // defer func() { // if r := recover(); r != nil { // os.Exit(0) // } // }() // cwd, err := os.Getwd() // if err != nil { // return err // } // args := os.Args // // for _, key := range args { // // fmt.Println(key) // // } // cmd := exec.Command(args[0], args[1:]...) // cmd.Stdout = os.Stdout // cmd.Stderr = os.Stderr // cmd.Dir = cwd // err = cmd.Start() // if err != nil { // return err // } // // cmd.Process.Release() // panic(fmt.Errorf("restarting")) // } func (ctx *appContext) Restart() error { defer func() { if r := recover(); r != nil { signal.Notify(ctx.quit, os.Interrupt) <-ctx.quit } }() args := os.Args // After a single restart, args[0] gets messed up and isnt the real executable. // JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable if os.Getenv("JFA_DEEP") == "" { os.Setenv("JFA_DEEP", "1") os.Setenv("JFA_EXEC", args[0]) } env := os.Environ() err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env) if err != nil { return err } panic(fmt.Errorf("restarting")) }