1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 17:10:10 +00:00
jfa-go/api.go
Harvey Tindall 4e16f6fd48 make checkInvite check only one invite, invite daemon
checkInvite no longer loops over all invites and checks for expiry, that
functionality has moved to checkInvites. Couple more rogue print
statements removed aswell.
2020-08-01 15:22:30 +01:00

577 lines
17 KiB
Go

package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/knz/strtime"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"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"] {
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"] {
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"] {
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})
ctx.loadConfig()
}