mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10:10 +00:00
parent
cebde9d4c0
commit
12db53d1eb
432
api-invites.go
Normal file
432
api-invites.go
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/itchyny/timefmt-go"
|
||||||
|
"github.com/lithammer/shortuuid/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *appContext) checkInvites() {
|
||||||
|
currentTime := time.Now()
|
||||||
|
app.storage.loadInvites()
|
||||||
|
changed := false
|
||||||
|
for code, data := range app.storage.invites {
|
||||||
|
expiry := data.ValidTill
|
||||||
|
if !currentTime.After(expiry) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||||
|
notify := data.Notify
|
||||||
|
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||||
|
app.debug.Printf("%s: Expiry notification", code)
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
for address, settings := range notify {
|
||||||
|
if !settings["notify-expiry"] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wait.Add(1)
|
||||||
|
go func(addr string) {
|
||||||
|
defer wait.Done()
|
||||||
|
msg, err := app.email.constructExpiry(code, data, app, false)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||||
|
} else {
|
||||||
|
// Check whether notify "address" is an email address of Jellyfin ID
|
||||||
|
if strings.Contains(addr, "@") {
|
||||||
|
err = app.email.send(msg, addr)
|
||||||
|
} else {
|
||||||
|
err = app.sendByID(msg, addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("Sent expiry notification to %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(address)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
delete(app.storage.invites, code)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
app.storage.storeInvites()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
||||||
|
currentTime := time.Now()
|
||||||
|
app.storage.loadInvites()
|
||||||
|
changed := false
|
||||||
|
inv, match := app.storage.invites[code]
|
||||||
|
if !match {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expiry := inv.ValidTill
|
||||||
|
if currentTime.After(expiry) {
|
||||||
|
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||||
|
notify := inv.Notify
|
||||||
|
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||||
|
app.debug.Printf("%s: Expiry notification", code)
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
for address, settings := range notify {
|
||||||
|
if !settings["notify-expiry"] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wait.Add(1)
|
||||||
|
go func(addr string) {
|
||||||
|
defer wait.Done()
|
||||||
|
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||||
|
} else {
|
||||||
|
// Check whether notify "address" is an email address of Jellyfin ID
|
||||||
|
if strings.Contains(addr, "@") {
|
||||||
|
err = app.email.send(msg, addr)
|
||||||
|
} else {
|
||||||
|
err = app.sendByID(msg, addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("Sent expiry notification to %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(address)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
match = false
|
||||||
|
delete(app.storage.invites, code)
|
||||||
|
} else if used {
|
||||||
|
changed = true
|
||||||
|
del := false
|
||||||
|
newInv := inv
|
||||||
|
if newInv.RemainingUses == 1 {
|
||||||
|
del = true
|
||||||
|
delete(app.storage.invites, code)
|
||||||
|
} else if newInv.RemainingUses != 0 {
|
||||||
|
// 0 means infinite i guess?
|
||||||
|
newInv.RemainingUses--
|
||||||
|
}
|
||||||
|
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
|
||||||
|
if !del {
|
||||||
|
app.storage.invites[code] = newInv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
app.storage.storeInvites()
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Create a new invite.
|
||||||
|
// @Produce json
|
||||||
|
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Router /invites [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Invites
|
||||||
|
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||||
|
var req generateInviteDTO
|
||||||
|
app.debug.Println("Generating new invite")
|
||||||
|
app.storage.loadInvites()
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
currentTime := time.Now()
|
||||||
|
validTill := currentTime.AddDate(0, req.Months, req.Days)
|
||||||
|
validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
|
||||||
|
// make sure code doesn't begin with number
|
||||||
|
inviteCode := shortuuid.New()
|
||||||
|
_, err := strconv.Atoi(string(inviteCode[0]))
|
||||||
|
for err == nil {
|
||||||
|
inviteCode = shortuuid.New()
|
||||||
|
_, err = strconv.Atoi(string(inviteCode[0]))
|
||||||
|
}
|
||||||
|
var invite Invite
|
||||||
|
if req.Label != "" {
|
||||||
|
invite.Label = req.Label
|
||||||
|
}
|
||||||
|
invite.Created = currentTime
|
||||||
|
if req.MultipleUses {
|
||||||
|
if req.NoLimit {
|
||||||
|
invite.NoLimit = true
|
||||||
|
} else {
|
||||||
|
invite.RemainingUses = req.RemainingUses
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
invite.RemainingUses = 1
|
||||||
|
}
|
||||||
|
invite.UserExpiry = req.UserExpiry
|
||||||
|
if invite.UserExpiry {
|
||||||
|
invite.UserMonths = req.UserMonths
|
||||||
|
invite.UserDays = req.UserDays
|
||||||
|
invite.UserHours = req.UserHours
|
||||||
|
invite.UserMinutes = req.UserMinutes
|
||||||
|
}
|
||||||
|
invite.ValidTill = validTill
|
||||||
|
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||||
|
addressValid := false
|
||||||
|
discord := ""
|
||||||
|
app.debug.Printf("%s: Sending invite message", inviteCode)
|
||||||
|
if discordEnabled && !strings.Contains(req.SendTo, "@") {
|
||||||
|
users := app.discord.GetUsers(req.SendTo)
|
||||||
|
if len(users) == 0 {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||||
|
} else if len(users) > 1 {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||||
|
} else {
|
||||||
|
invite.SendTo = req.SendTo
|
||||||
|
addressValid = true
|
||||||
|
discord = users[0].User.ID
|
||||||
|
}
|
||||||
|
} else if emailEnabled {
|
||||||
|
addressValid = true
|
||||||
|
invite.SendTo = req.SendTo
|
||||||
|
}
|
||||||
|
if addressValid {
|
||||||
|
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
||||||
|
if err != nil {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
|
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
if discord != "" {
|
||||||
|
err = app.discord.SendDM(msg, discord)
|
||||||
|
} else {
|
||||||
|
err = app.email.send(msg, req.SendTo)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
|
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Profile != "" {
|
||||||
|
if _, ok := app.storage.profiles[req.Profile]; ok {
|
||||||
|
invite.Profile = req.Profile
|
||||||
|
} else {
|
||||||
|
invite.Profile = "Default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.storage.invites[inviteCode] = invite
|
||||||
|
app.storage.storeInvites()
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get invites.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} getInvitesDTO
|
||||||
|
// @Router /invites [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Invites
|
||||||
|
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||||
|
app.debug.Println("Invites requested")
|
||||||
|
currentTime := time.Now()
|
||||||
|
app.storage.loadInvites()
|
||||||
|
app.checkInvites()
|
||||||
|
var invites []inviteDTO
|
||||||
|
for code, inv := range app.storage.invites {
|
||||||
|
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
||||||
|
invite := inviteDTO{
|
||||||
|
Code: code,
|
||||||
|
Months: months,
|
||||||
|
Days: days,
|
||||||
|
Hours: hours,
|
||||||
|
Minutes: minutes,
|
||||||
|
UserExpiry: inv.UserExpiry,
|
||||||
|
UserMonths: inv.UserMonths,
|
||||||
|
UserDays: inv.UserDays,
|
||||||
|
UserHours: inv.UserHours,
|
||||||
|
UserMinutes: inv.UserMinutes,
|
||||||
|
Created: inv.Created.Unix(),
|
||||||
|
Profile: inv.Profile,
|
||||||
|
NoLimit: inv.NoLimit,
|
||||||
|
Label: inv.Label,
|
||||||
|
}
|
||||||
|
if len(inv.UsedBy) != 0 {
|
||||||
|
invite.UsedBy = map[string]int64{}
|
||||||
|
for _, pair := range inv.UsedBy {
|
||||||
|
// These used to be stored formatted instead of as a unix timestamp.
|
||||||
|
unix, err := strconv.ParseInt(pair[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to parse usedBy time: %v", err)
|
||||||
|
}
|
||||||
|
unix = date.Unix()
|
||||||
|
}
|
||||||
|
invite.UsedBy[pair[0]] = unix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invite.RemainingUses = 1
|
||||||
|
if inv.RemainingUses != 0 {
|
||||||
|
invite.RemainingUses = inv.RemainingUses
|
||||||
|
}
|
||||||
|
if inv.SendTo != "" {
|
||||||
|
invite.SendTo = inv.SendTo
|
||||||
|
}
|
||||||
|
if len(inv.Notify) != 0 {
|
||||||
|
var address string
|
||||||
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||||
|
app.storage.loadEmails()
|
||||||
|
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
|
||||||
|
address = addr.Addr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
address = app.config.Section("ui").Key("email").String()
|
||||||
|
}
|
||||||
|
if _, ok := inv.Notify[address]; ok {
|
||||||
|
if _, ok = inv.Notify[address]["notify-expiry"]; ok {
|
||||||
|
invite.NotifyExpiry = inv.Notify[address]["notify-expiry"]
|
||||||
|
}
|
||||||
|
if _, ok = inv.Notify[address]["notify-creation"]; ok {
|
||||||
|
invite.NotifyCreation = inv.Notify[address]["notify-creation"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invites = append(invites, invite)
|
||||||
|
}
|
||||||
|
profiles := make([]string, len(app.storage.profiles))
|
||||||
|
if len(app.storage.profiles) != 0 {
|
||||||
|
profiles[0] = app.storage.defaultProfile
|
||||||
|
i := 1
|
||||||
|
if len(app.storage.profiles) > 1 {
|
||||||
|
for p := range app.storage.profiles {
|
||||||
|
if p != app.storage.defaultProfile {
|
||||||
|
profiles[i] = p
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp := getInvitesDTO{
|
||||||
|
Profiles: profiles,
|
||||||
|
Invites: invites,
|
||||||
|
}
|
||||||
|
gc.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Set profile for an invite
|
||||||
|
// @Produce json
|
||||||
|
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /invites/profile [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Profiles & Settings
|
||||||
|
func (app *appContext) SetProfile(gc *gin.Context) {
|
||||||
|
var req inviteProfileDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
||||||
|
// "" means "Don't apply profile"
|
||||||
|
if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" {
|
||||||
|
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
||||||
|
respond(500, "Profile not found", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inv := app.storage.invites[req.Invite]
|
||||||
|
inv.Profile = req.Profile
|
||||||
|
app.storage.invites[req.Invite] = inv
|
||||||
|
app.storage.storeInvites()
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Set notification preferences for an invite.
|
||||||
|
// @Produce json
|
||||||
|
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 400 {object} stringResponse
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /invites/notify [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) SetNotify(gc *gin.Context) {
|
||||||
|
var req map[string]map[string]bool
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
changed := false
|
||||||
|
for code, settings := range req {
|
||||||
|
app.debug.Printf("%s: Notification settings change requested", code)
|
||||||
|
app.storage.loadInvites()
|
||||||
|
app.storage.loadEmails()
|
||||||
|
invite, ok := app.storage.invites[code]
|
||||||
|
if !ok {
|
||||||
|
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||||
|
respond(400, "Invalid invite code", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var address string
|
||||||
|
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(false)
|
||||||
|
if jellyfinLogin {
|
||||||
|
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
|
||||||
|
if !addressAvailable {
|
||||||
|
app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code)
|
||||||
|
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||||
|
respond(500, "Missing user contact method", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
address = gc.GetString("jfId")
|
||||||
|
} else {
|
||||||
|
address = app.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 _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
||||||
|
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
||||||
|
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
|
||||||
|
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
|
||||||
|
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
app.storage.invites[code] = invite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
app.storage.storeInvites()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Delete an invite.
|
||||||
|
// @Produce json
|
||||||
|
// @Param deleteInviteDTO body deleteInviteDTO true "Delete invite object"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} stringResponse
|
||||||
|
// @Router /invites [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Invites
|
||||||
|
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||||
|
var req deleteInviteDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||||
|
var ok bool
|
||||||
|
_, ok = app.storage.invites[req.Code]
|
||||||
|
if ok {
|
||||||
|
delete(app.storage.invites, req.Code)
|
||||||
|
app.storage.storeInvites()
|
||||||
|
app.info.Printf("%s: Invite deleted", req.Code)
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
||||||
|
respond(400, "Code doesn't exist", gc)
|
||||||
|
}
|
700
api-messages.go
Normal file
700
api-messages.go
Normal file
@ -0,0 +1,700 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Get a list of email names and IDs.
|
||||||
|
// @Produce json
|
||||||
|
// @Param lang query string false "Language for email titles."
|
||||||
|
// @Success 200 {object} emailListDTO
|
||||||
|
// @Router /config/emails [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Configuration
|
||||||
|
func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||||
|
lang := gc.Query("lang")
|
||||||
|
if _, ok := app.storage.lang.Email[lang]; !ok {
|
||||||
|
lang = app.storage.lang.chosenEmailLang
|
||||||
|
}
|
||||||
|
gc.JSON(200, emailListDTO{
|
||||||
|
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
|
||||||
|
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
|
||||||
|
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
|
||||||
|
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled},
|
||||||
|
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.customEmails.UserDisabled.Enabled},
|
||||||
|
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.customEmails.UserEnabled.Enabled},
|
||||||
|
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled},
|
||||||
|
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
|
||||||
|
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
|
||||||
|
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) getCustomEmail(id string) *customEmail {
|
||||||
|
switch id {
|
||||||
|
case "Announcement":
|
||||||
|
return &customEmail{}
|
||||||
|
case "UserCreated":
|
||||||
|
return &app.storage.customEmails.UserCreated
|
||||||
|
case "InviteExpiry":
|
||||||
|
return &app.storage.customEmails.InviteExpiry
|
||||||
|
case "PasswordReset":
|
||||||
|
return &app.storage.customEmails.PasswordReset
|
||||||
|
case "UserDeleted":
|
||||||
|
return &app.storage.customEmails.UserDeleted
|
||||||
|
case "UserDisabled":
|
||||||
|
return &app.storage.customEmails.UserDisabled
|
||||||
|
case "UserEnabled":
|
||||||
|
return &app.storage.customEmails.UserEnabled
|
||||||
|
case "InviteEmail":
|
||||||
|
return &app.storage.customEmails.InviteEmail
|
||||||
|
case "WelcomeEmail":
|
||||||
|
return &app.storage.customEmails.WelcomeEmail
|
||||||
|
case "EmailConfirmation":
|
||||||
|
return &app.storage.customEmails.EmailConfirmation
|
||||||
|
case "UserExpired":
|
||||||
|
return &app.storage.customEmails.UserExpired
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Sets the corresponding custom email.
|
||||||
|
// @Produce json
|
||||||
|
// @Param customEmail body customEmail true "Content = email (in markdown)."
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param id path string true "ID of email"
|
||||||
|
// @Router /config/emails/{id} [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Configuration
|
||||||
|
func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||||
|
var req customEmail
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
id := gc.Param("id")
|
||||||
|
if req.Content == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email := app.getCustomEmail(id)
|
||||||
|
if email == nil {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email.Content = req.Content
|
||||||
|
email.Enabled = true
|
||||||
|
if app.storage.storeCustomEmails() != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Enable/Disable custom email.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param enable/disable path string true "enable/disable"
|
||||||
|
// @Param id path string true "ID of email"
|
||||||
|
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Configuration
|
||||||
|
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||||
|
id := gc.Param("id")
|
||||||
|
s := gc.Param("state")
|
||||||
|
enabled := false
|
||||||
|
if s == "enable" {
|
||||||
|
enabled = true
|
||||||
|
} else if s != "disable" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
}
|
||||||
|
email := app.getCustomEmail(id)
|
||||||
|
if email == nil {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email.Enabled = enabled
|
||||||
|
if app.storage.storeCustomEmails() != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} customEmailDTO
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param id path string true "ID of email"
|
||||||
|
// @Router /config/emails/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Configuration
|
||||||
|
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||||
|
lang := app.storage.lang.chosenEmailLang
|
||||||
|
id := gc.Param("id")
|
||||||
|
var content string
|
||||||
|
var err error
|
||||||
|
var msg *Message
|
||||||
|
var variables []string
|
||||||
|
var conditionals []string
|
||||||
|
var values map[string]interface{}
|
||||||
|
username := app.storage.lang.Email[lang].Strings.get("username")
|
||||||
|
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||||
|
email := app.getCustomEmail(id)
|
||||||
|
if email == nil {
|
||||||
|
app.err.Printf("Failed to get custom email with ID \"%s\"", id)
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if id == "WelcomeEmail" {
|
||||||
|
conditionals = []string{"{yourAccountWillExpire}"}
|
||||||
|
email.Conditionals = conditionals
|
||||||
|
}
|
||||||
|
content = email.Content
|
||||||
|
noContent := content == ""
|
||||||
|
if !noContent {
|
||||||
|
variables = email.Variables
|
||||||
|
}
|
||||||
|
switch id {
|
||||||
|
case "Announcement":
|
||||||
|
// Just send the email html
|
||||||
|
content = ""
|
||||||
|
case "UserCreated":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructCreated("", "", "", Invite{}, app, true)
|
||||||
|
}
|
||||||
|
values = app.email.createdValues("xxxxxx", username, emailAddress, Invite{}, app, false)
|
||||||
|
case "InviteExpiry":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructExpiry("", Invite{}, app, true)
|
||||||
|
}
|
||||||
|
values = app.email.expiryValues("xxxxxx", Invite{}, app, false)
|
||||||
|
case "PasswordReset":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructReset(PasswordReset{}, app, true)
|
||||||
|
}
|
||||||
|
values = app.email.resetValues(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
|
||||||
|
case "UserDeleted":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructDeleted("", app, true)
|
||||||
|
}
|
||||||
|
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
|
||||||
|
case "UserDisabled":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructDisabled("", app, true)
|
||||||
|
}
|
||||||
|
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
|
||||||
|
case "UserEnabled":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructEnabled("", app, true)
|
||||||
|
}
|
||||||
|
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
|
||||||
|
case "InviteEmail":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructInvite("", Invite{}, app, true)
|
||||||
|
}
|
||||||
|
values = app.email.inviteValues("xxxxxx", Invite{}, app, false)
|
||||||
|
case "WelcomeEmail":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructWelcome("", time.Time{}, app, true)
|
||||||
|
}
|
||||||
|
values = app.email.welcomeValues(username, time.Now(), app, false, true)
|
||||||
|
case "EmailConfirmation":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructConfirmation("", "", "", app, true)
|
||||||
|
}
|
||||||
|
values = app.email.confirmationValues("xxxxxx", username, "xxxxxx", app, false)
|
||||||
|
case "UserExpired":
|
||||||
|
if noContent {
|
||||||
|
msg, err = app.email.constructUserExpired(app, true)
|
||||||
|
}
|
||||||
|
values = app.email.userExpiredValues(app, false)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if noContent && id != "Announcement" {
|
||||||
|
content = msg.Text
|
||||||
|
variables = make([]string, strings.Count(content, "{"))
|
||||||
|
i := 0
|
||||||
|
found := false
|
||||||
|
buf := ""
|
||||||
|
for _, c := range content {
|
||||||
|
if !found && c != '{' && c != '}' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
buf += string(c)
|
||||||
|
if c == '}' {
|
||||||
|
found = false
|
||||||
|
variables[i] = buf
|
||||||
|
buf = ""
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
email.Variables = variables
|
||||||
|
}
|
||||||
|
if variables == nil {
|
||||||
|
variables = []string{}
|
||||||
|
}
|
||||||
|
if app.storage.storeCustomEmails() != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mail, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
||||||
|
if err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns a new Telegram verification PIN, and the bot username.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} telegramPinDTO
|
||||||
|
// @Router /telegram/pin [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) TelegramGetPin(gc *gin.Context) {
|
||||||
|
gc.JSON(200, telegramPinDTO{
|
||||||
|
Token: app.telegram.NewAuthToken(),
|
||||||
|
Username: app.telegram.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Link a Jellyfin & Telegram user together via a verification PIN.
|
||||||
|
// @Produce json
|
||||||
|
// @Param telegramSetDTO body telegramSetDTO true "Token and user's Jellyfin ID."
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Router /users/telegram [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||||
|
var req telegramSetDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.Token == "" || req.ID == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenIndex := -1
|
||||||
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
|
if v.Token == req.Token {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokenIndex == -1 {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tgToken := app.telegram.verifiedTokens[tokenIndex]
|
||||||
|
tgUser := TelegramUser{
|
||||||
|
ChatID: tgToken.ChatID,
|
||||||
|
Username: tgToken.Username,
|
||||||
|
Contact: true,
|
||||||
|
}
|
||||||
|
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
||||||
|
tgUser.Lang = lang
|
||||||
|
}
|
||||||
|
if app.storage.telegram == nil {
|
||||||
|
app.storage.telegram = map[string]TelegramUser{}
|
||||||
|
}
|
||||||
|
app.storage.telegram[req.ID] = tgUser
|
||||||
|
err := app.storage.storeTelegramUsers()
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||||
|
} else {
|
||||||
|
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
|
||||||
|
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||||
|
}
|
||||||
|
linkExistingOmbiDiscordTelegram(app)
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Sets whether to notify a user through telegram/discord/matrix/email or not.
|
||||||
|
// @Produce json
|
||||||
|
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Success 400 {object} boolResponse
|
||||||
|
// @Success 500 {object} boolResponse
|
||||||
|
// @Router /users/telegram/notify [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||||
|
var req SetContactMethodsDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.ID == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||||
|
change := tgUser.Contact != req.Telegram
|
||||||
|
tgUser.Contact = req.Telegram
|
||||||
|
app.storage.telegram[req.ID] = tgUser
|
||||||
|
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Telegram: Failed to store users: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if change {
|
||||||
|
msg := ""
|
||||||
|
if !req.Telegram {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dcUser, ok := app.storage.discord[req.ID]; ok {
|
||||||
|
change := dcUser.Contact != req.Discord
|
||||||
|
dcUser.Contact = req.Discord
|
||||||
|
app.storage.discord[req.ID] = dcUser
|
||||||
|
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Discord: Failed to store users: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if change {
|
||||||
|
msg := ""
|
||||||
|
if !req.Discord {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mxUser, ok := app.storage.matrix[req.ID]; ok {
|
||||||
|
change := mxUser.Contact != req.Matrix
|
||||||
|
mxUser.Contact = req.Matrix
|
||||||
|
app.storage.matrix[req.ID] = mxUser
|
||||||
|
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Matrix: Failed to store users: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if change {
|
||||||
|
msg := ""
|
||||||
|
if !req.Matrix {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if email, ok := app.storage.emails[req.ID]; ok {
|
||||||
|
change := email.Contact != req.Email
|
||||||
|
email.Contact = req.Email
|
||||||
|
app.storage.emails[req.ID] = email
|
||||||
|
if err := app.storage.storeEmails(); err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Failed to store emails: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if change {
|
||||||
|
msg := ""
|
||||||
|
if !req.Email {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Param pin path string true "PIN code to check"
|
||||||
|
// @Router /telegram/verified/{pin} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||||
|
pin := gc.Param("pin")
|
||||||
|
tokenIndex := -1
|
||||||
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
|
if v.Token == pin {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if tokenIndex != -1 {
|
||||||
|
// length := len(app.telegram.verifiedTokens)
|
||||||
|
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
|
||||||
|
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
|
||||||
|
// }
|
||||||
|
respondBool(200, tokenIndex != -1, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Success 401 {object} boolResponse
|
||||||
|
// @Param pin path string true "PIN code to check"
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Router /invite/{invCode}/telegram/verified/{pin} [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin := gc.Param("pin")
|
||||||
|
tokenIndex := -1
|
||||||
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
|
if v.Token == pin {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if tokenIndex != -1 {
|
||||||
|
// length := len(app.telegram.verifiedTokens)
|
||||||
|
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
|
||||||
|
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
|
||||||
|
// }
|
||||||
|
respondBool(200, tokenIndex != -1, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Param pin path string true "PIN code to check"
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Router /invite/{invCode}/discord/verified/{pin} [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin := gc.Param("pin")
|
||||||
|
_, ok := app.discord.verifiedTokens[pin]
|
||||||
|
respondBool(200, ok, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns a 10-minute, one-use Discord server invite
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} DiscordInviteDTO
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Router /invite/{invCode}/discord/invite [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||||
|
if app.discord.inviteChannelName == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
|
||||||
|
if invURL == "" {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Generate and send a new PIN to a specified Matrix user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
||||||
|
// @Router /invite/{invCode}/matrix/user [post]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req MatrixSendPINDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.UserID == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ok := app.matrix.SendStart(req.UserID)
|
||||||
|
if !ok {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Param pin path string true "PIN code to check"
|
||||||
|
// @Param invCode path string true "invite Code"
|
||||||
|
// @Param userID path string true "Matrix User ID"
|
||||||
|
// @Router /invite/{invCode}/matrix/verified/{userID}/{pin} [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||||
|
code := gc.Param("invCode")
|
||||||
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
|
app.debug.Println("Matrix: Invite code was invalid")
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := gc.Param("userID")
|
||||||
|
pin := gc.Param("pin")
|
||||||
|
user, ok := app.matrix.tokens[pin]
|
||||||
|
if !ok {
|
||||||
|
app.debug.Println("Matrix: PIN not found")
|
||||||
|
respondBool(200, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.User.UserID != userID {
|
||||||
|
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||||
|
respondBool(200, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Verified = true
|
||||||
|
app.matrix.tokens[pin] = user
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Generates a Matrix access token from a username and password.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} stringResponse
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
|
||||||
|
// @Router /matrix/login [post]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||||
|
var req MatrixLoginDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.Username == "" || req.Password == "" {
|
||||||
|
respond(400, "errorLoginBlank", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||||
|
respond(401, "Unauthorized", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tempConfig, _ := ini.Load(app.configPath)
|
||||||
|
matrix := tempConfig.Section("matrix")
|
||||||
|
matrix.Key("enabled").SetValue("true")
|
||||||
|
matrix.Key("homeserver").SetValue(req.Homeserver)
|
||||||
|
matrix.Key("token").SetValue(token)
|
||||||
|
matrix.Key("user_id").SetValue(req.Username)
|
||||||
|
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||||
|
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Links a Matrix user to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
|
||||||
|
// @Router /users/matrix [post]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||||
|
var req MatrixConnectUserDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if app.storage.matrix == nil {
|
||||||
|
app.storage.matrix = map[string]MatrixUser{}
|
||||||
|
}
|
||||||
|
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.storage.matrix[req.JellyfinID] = MatrixUser{
|
||||||
|
UserID: req.UserID,
|
||||||
|
RoomID: string(roomID),
|
||||||
|
Lang: "en-us",
|
||||||
|
Contact: true,
|
||||||
|
Encrypted: encrypted,
|
||||||
|
}
|
||||||
|
app.matrix.isEncrypted[roomID] = encrypted
|
||||||
|
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||||
|
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} DiscordUsersDTO
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param username path string true "username to search."
|
||||||
|
// @Router /users/discord/{username} [get]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
|
||||||
|
name := gc.Param("username")
|
||||||
|
if name == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
users := app.discord.GetUsers(name)
|
||||||
|
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
|
||||||
|
for i, u := range users {
|
||||||
|
resp.Users[i] = DiscordUserDTO{
|
||||||
|
Name: u.User.Username + "#" + u.User.Discriminator,
|
||||||
|
ID: u.User.ID,
|
||||||
|
AvatarURL: u.User.AvatarURL("32"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gc.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
|
||||||
|
// @Router /users/discord [post]
|
||||||
|
// @tags Other
|
||||||
|
func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||||
|
var req DiscordConnectUserDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.JellyfinID == "" || req.DiscordID == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, ok := app.discord.NewUser(req.DiscordID)
|
||||||
|
if !ok {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.storage.discord[req.JellyfinID] = user
|
||||||
|
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||||
|
app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
linkExistingOmbiDiscordTelegram(app)
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
119
api-ombi.go
Normal file
119
api-ombi.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
||||||
|
ombiUsers, code, err := app.ombi.GetUsers()
|
||||||
|
if err != nil || code != 200 {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
|
jfUser, code, err := app.jf.UserByID(jfID, false)
|
||||||
|
if err != nil || code != 200 {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
|
username := jfUser.Name
|
||||||
|
email := ""
|
||||||
|
if e, ok := app.storage.emails[jfID]; ok {
|
||||||
|
email = e.Addr
|
||||||
|
}
|
||||||
|
for _, ombiUser := range ombiUsers {
|
||||||
|
ombiAddr := ""
|
||||||
|
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
||||||
|
ombiAddr = a.(string)
|
||||||
|
}
|
||||||
|
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
|
||||||
|
return ombiUser, code, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, 400, fmt.Errorf("Couldn't find user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get a list of Ombi users.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} ombiUsersDTO
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /ombi/users [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Ombi
|
||||||
|
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||||
|
app.debug.Println("Ombi users requested")
|
||||||
|
users, status, err := app.ombi.GetUsers()
|
||||||
|
if err != nil || status != 200 {
|
||||||
|
app.err.Printf("Failed to get users from Ombi (%d): %v", status, err)
|
||||||
|
respond(500, "Couldn't get users", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userlist := make([]ombiUser, len(users))
|
||||||
|
for i, data := range users {
|
||||||
|
userlist[i] = ombiUser{
|
||||||
|
Name: data["userName"].(string),
|
||||||
|
ID: data["id"].(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gc.JSON(200, ombiUsersDTO{Users: userlist})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Store Ombi user template in an existing profile.
|
||||||
|
// @Produce json
|
||||||
|
// @Param ombiUser body ombiUser true "User to source settings from"
|
||||||
|
// @Param profile path string true "Name of profile to store in"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /profiles/ombi/{profile} [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Ombi
|
||||||
|
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||||
|
var req ombiUser
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
profileName := gc.Param("profile")
|
||||||
|
profile, ok := app.storage.profiles[profileName]
|
||||||
|
if !ok {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template, code, err := app.ombi.TemplateByID(req.ID)
|
||||||
|
if err != nil || code != 200 || len(template) == 0 {
|
||||||
|
app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err)
|
||||||
|
respond(500, "Couldn't get user", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile.Ombi = template
|
||||||
|
app.storage.profiles[profileName] = profile
|
||||||
|
if err := app.storage.storeProfiles(); err != nil {
|
||||||
|
respond(500, "Failed to store profile", gc)
|
||||||
|
app.err.Printf("Failed to store profiles: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(204, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Remove ombi user template from a profile.
|
||||||
|
// @Produce json
|
||||||
|
// @Param profile path string true "Name of profile to store in"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /profiles/ombi/{profile} [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Ombi
|
||||||
|
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
||||||
|
profileName := gc.Param("profile")
|
||||||
|
profile, ok := app.storage.profiles[profileName]
|
||||||
|
if !ok {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile.Ombi = nil
|
||||||
|
app.storage.profiles[profileName] = profile
|
||||||
|
if err := app.storage.storeProfiles(); err != nil {
|
||||||
|
respond(500, "Failed to store profile", gc)
|
||||||
|
app.err.Printf("Failed to store profiles: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondBool(204, true, gc)
|
||||||
|
}
|
121
api-profiles.go
Normal file
121
api-profiles.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Get a list of profiles
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} getProfilesDTO
|
||||||
|
// @Router /profiles [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Profiles & Settings
|
||||||
|
func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||||
|
app.storage.loadProfiles()
|
||||||
|
app.debug.Println("Profiles requested")
|
||||||
|
out := getProfilesDTO{
|
||||||
|
DefaultProfile: app.storage.defaultProfile,
|
||||||
|
Profiles: map[string]profileDTO{},
|
||||||
|
}
|
||||||
|
for name, p := range app.storage.profiles {
|
||||||
|
out.Profiles[name] = profileDTO{
|
||||||
|
Admin: p.Admin,
|
||||||
|
LibraryAccess: p.LibraryAccess,
|
||||||
|
FromUser: p.FromUser,
|
||||||
|
Ombi: p.Ombi != nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gc.JSON(200, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Set the default profile to use.
|
||||||
|
// @Produce json
|
||||||
|
// @Param profileChangeDTO body profileChangeDTO true "Default profile object"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /profiles/default [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Profiles & Settings
|
||||||
|
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||||
|
req := profileChangeDTO{}
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
app.info.Printf("Setting default profile to \"%s\"", req.Name)
|
||||||
|
if _, ok := app.storage.profiles[req.Name]; !ok {
|
||||||
|
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
||||||
|
respond(500, "Profile not found", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for name, profile := range app.storage.profiles {
|
||||||
|
if name == req.Name {
|
||||||
|
profile.Admin = true
|
||||||
|
app.storage.profiles[name] = profile
|
||||||
|
} else {
|
||||||
|
profile.Admin = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.storage.defaultProfile = req.Name
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Create a profile based on a Jellyfin user's settings.
|
||||||
|
// @Produce json
|
||||||
|
// @Param newProfileDTO body newProfileDTO true "New profile object"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /profiles [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Profiles & Settings
|
||||||
|
func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||||
|
app.info.Println("Profile creation requested")
|
||||||
|
var req newProfileDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
app.jf.CacheExpiry = time.Now()
|
||||||
|
user, status, err := app.jf.UserByID(req.ID, false)
|
||||||
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
|
app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err)
|
||||||
|
respond(500, "Couldn't get user", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile := Profile{
|
||||||
|
FromUser: user.Name,
|
||||||
|
Policy: user.Policy,
|
||||||
|
}
|
||||||
|
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
|
||||||
|
if req.Homescreen {
|
||||||
|
profile.Configuration = user.Configuration
|
||||||
|
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||||
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
|
app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err)
|
||||||
|
respond(500, "Couldn't get displayprefs", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.storage.loadProfiles()
|
||||||
|
app.storage.profiles[req.Name] = profile
|
||||||
|
app.storage.storeProfiles()
|
||||||
|
app.storage.loadProfiles()
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Delete an existing profile
|
||||||
|
// @Produce json
|
||||||
|
// @Param profileChangeDTO body profileChangeDTO true "Delete profile object"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Router /profiles [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Profiles & Settings
|
||||||
|
func (app *appContext) DeleteProfile(gc *gin.Context) {
|
||||||
|
req := profileChangeDTO{}
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
name := req.Name
|
||||||
|
if _, ok := app.storage.profiles[name]; ok {
|
||||||
|
if app.storage.defaultProfile == name {
|
||||||
|
app.storage.defaultProfile = ""
|
||||||
|
}
|
||||||
|
delete(app.storage.profiles, name)
|
||||||
|
}
|
||||||
|
app.storage.storeProfiles()
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
1140
api-users.go
Normal file
1140
api-users.go
Normal file
File diff suppressed because it is too large
Load Diff
2
go.mod
2
go.mod
@ -12,6 +12,8 @@ replace github.com/hrfee/jfa-go/logger => ./logger
|
|||||||
|
|
||||||
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
||||||
|
|
||||||
|
replace github.com/hrfee/jfa-go/api => ./api
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820
|
github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820
|
||||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
"errorInvalidUserPass": "Invalid username/password.",
|
"errorInvalidUserPass": "Invalid username/password.",
|
||||||
"errorNotAdmin": "User is not allowed to manage server.",
|
"errorNotAdmin": "User is not allowed to manage server.",
|
||||||
"errorUserDisabled": "User may be disabled.",
|
"errorUserDisabled": "User may be disabled.",
|
||||||
"error404": "404, check the internal URL."
|
"error404": "404, check the internal URL.",
|
||||||
|
"errorConnectionRefused": "Connection refused."
|
||||||
},
|
},
|
||||||
"startPage": {
|
"startPage": {
|
||||||
"welcome": "Welcome!",
|
"welcome": "Welcome!",
|
||||||
|
3
setup.go
3
setup.go
@ -68,6 +68,9 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
|||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
msg := ""
|
msg := ""
|
||||||
switch status {
|
switch status {
|
||||||
|
case 0:
|
||||||
|
msg = "errorConnectionRefused"
|
||||||
|
status = 500
|
||||||
case 401:
|
case 401:
|
||||||
msg = "errorInvalidUserPass"
|
msg = "errorInvalidUserPass"
|
||||||
case 403:
|
case 403:
|
||||||
|
Loading…
Reference in New Issue
Block a user