a08a0fd3e6
When generating the cache and calling userSummary per user, previously the DB was queried for an invite with the ReferrerJellyfinID set to the user's ID. This was fast enough on my test system (~5000 users in ~1.5s), while testing cross-compilation, I found it ran extremely slow on an arm64 build (running through QEMU admittedly), doing ~5000 in ~18s. Instead, a map of IDs to invites/referrals is generated once and queried instead. Initial load now takes ~80ms on my system, and 0.95s through QEMU, and 0.68s on a rockpro64 SBC.
239 lines
6.3 KiB
Go
239 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/hrfee/jfa-go/logger"
|
|
lm "github.com/hrfee/jfa-go/logmessages"
|
|
"github.com/hrfee/mediabrowser"
|
|
"github.com/lithammer/shortuuid/v3"
|
|
)
|
|
|
|
// ReponseFunc responds to the user, generally by HTTP response
|
|
// The cases when more than this occurs are given below.
|
|
type ResponseFunc func(gc *gin.Context)
|
|
|
|
// LogFunc prints a log line once called.
|
|
type LogFunc func()
|
|
|
|
type ContactMethodConf struct {
|
|
Email, Discord, Telegram, Matrix bool
|
|
}
|
|
type ContactMethodUsers struct {
|
|
Email emailStore
|
|
Discord DiscordUser
|
|
Telegram TelegramVerifiedToken
|
|
Matrix MatrixUser
|
|
}
|
|
|
|
type ContactMethodValidation struct {
|
|
Verified ContactMethodConf
|
|
Users ContactMethodUsers
|
|
}
|
|
|
|
type NewUserParams struct {
|
|
Req newUserDTO
|
|
SourceType ActivitySource
|
|
Source string
|
|
ContextForIPLogging *gin.Context
|
|
Profile *Profile
|
|
}
|
|
|
|
type NewUserData struct {
|
|
Created bool
|
|
Success bool
|
|
User mediabrowser.User
|
|
Message string
|
|
Status int
|
|
Log func()
|
|
}
|
|
|
|
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
|
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData, pendingTasks *sync.WaitGroup) {
|
|
pendingTasks = &sync.WaitGroup{}
|
|
// Some helper functions which will behave as our app.info/error/debug
|
|
// And make sure we capture the correct caller location.
|
|
deferLogInfo := func(s string, args ...any) {
|
|
loc := logger.Lshortfile(2)
|
|
out.Log = func() {
|
|
app.info.PrintfNoFile(loc+" "+s, args...)
|
|
}
|
|
}
|
|
/* deferLogDebug := func(s string, args ...any) {
|
|
out.Log = func() {
|
|
app.debug.Printf(s, args)
|
|
}
|
|
} */
|
|
deferLogError := func(s string, args ...any) {
|
|
loc := logger.Lshortfile(2)
|
|
out.Log = func() {
|
|
app.err.PrintfNoFile(loc+" "+s, args...)
|
|
}
|
|
}
|
|
|
|
if strings.ContainsRune(p.Req.Username, '+') {
|
|
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, fmt.Sprintf(lm.InvalidChar, '+'))
|
|
out.Status = 400
|
|
out.Message = "errorSpecialSymbols"
|
|
return
|
|
}
|
|
|
|
existingUser, _ := app.jf.UserByName(p.Req.Username, false)
|
|
if existingUser.Name != "" {
|
|
out.Message = lm.UserExists
|
|
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
|
out.Status = 401
|
|
return
|
|
}
|
|
|
|
var err error
|
|
out.User, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
|
if err != nil {
|
|
out.Message = err.Error()
|
|
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
|
out.Status = 401
|
|
return
|
|
}
|
|
out.Created = true
|
|
// Invalidate cache to be safe
|
|
app.InvalidateUserCaches()
|
|
|
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
|
Type: ActivityCreation,
|
|
UserID: out.User.ID,
|
|
SourceType: p.SourceType,
|
|
Source: p.Source,
|
|
InviteCode: p.Req.Code, // Left blank when an admin does this
|
|
Value: out.User.Name,
|
|
Time: time.Now(),
|
|
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
|
|
|
|
if p.Profile != nil {
|
|
err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
|
}
|
|
err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
|
if err == nil {
|
|
err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
|
}
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
|
|
}
|
|
|
|
for _, tps := range app.thirdPartyServices {
|
|
if !tps.Enabled(app, p.Profile) {
|
|
continue
|
|
}
|
|
// When ok and err != nil, its a non-fatal failure that we lot without the "FailedImportUser".
|
|
err, ok := tps.ImportUser(out.User.ID, p.Req, *p.Profile)
|
|
if !ok {
|
|
app.err.Printf(lm.FailedImportUser, tps.Name(), p.Req.Username, err)
|
|
} else if err != nil {
|
|
app.info.Println(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|")
|
|
if len(webhookURIs) != 0 {
|
|
summary := app.userSummary(out.User, nil)
|
|
for _, uri := range webhookURIs {
|
|
pendingTasks.Add(1)
|
|
go func() {
|
|
app.webhooks.Send(uri, summary)
|
|
pendingTasks.Done()
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Welcome email is sent by each user of this method separately.
|
|
|
|
out.Status = 200
|
|
out.Success = true
|
|
app.InvalidateWebUserCache()
|
|
return
|
|
}
|
|
|
|
func (app *appContext) WelcomeNewUser(user mediabrowser.User, expiry time.Time) (failed bool) {
|
|
if !app.config.Section("welcome_email").Key("enabled").MustBool(false) {
|
|
// we didn't "fail", we "politely declined"
|
|
// failed = true
|
|
return
|
|
}
|
|
failed = true
|
|
name := app.getAddressOrName(user.ID)
|
|
if name == "" {
|
|
return
|
|
}
|
|
msg, err := app.email.constructWelcome(user.Name, expiry, false)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
|
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
|
app.err.Printf(lm.FailedSendWelcomeMessage, user.ID, name, err)
|
|
} else {
|
|
app.info.Printf(lm.SentWelcomeMessage, user.ID, name)
|
|
failed = false
|
|
}
|
|
return
|
|
}
|
|
|
|
func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (err error, change bool, activityType ActivityType) {
|
|
activityType = ActivityEnabled
|
|
if disabled {
|
|
activityType = ActivityDisabled
|
|
}
|
|
change = user.Policy.IsDisabled != disabled
|
|
user.Policy.IsDisabled = disabled
|
|
|
|
err = app.jf.SetPolicy(user.ID, user.Policy)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
|
|
cmUser, ok := app.storage.GetDiscordKey(user.ID)
|
|
if ok {
|
|
if err := app.discord.SetRoleDisabled(cmUser.MethodID().(string), disabled); err != nil {
|
|
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
|
if app.ombi != nil {
|
|
var tpUser map[string]any
|
|
tpUser, err = app.getOmbiUser(user.ID)
|
|
if err == nil {
|
|
if id, ok := tpUser["id"]; ok {
|
|
err = app.ombi.DeleteUser(id.(string))
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
|
|
cmUser, ok := app.storage.GetDiscordKey(user.ID)
|
|
if ok {
|
|
if err := app.discord.RemoveRole(cmUser.MethodID().(string)); err != nil {
|
|
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
err = app.jf.DeleteUser(user.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
deleted = true
|
|
return
|
|
}
|