Files
ggjuku/internal/app/account_sources.go
T
2026-09-30 10:18:32 +08:00

391 lines
11 KiB
Go

package app
import (
"context"
"errors"
"net/http"
"strings"
"time"
)
type accountSourceChoice struct {
ID string `json:"id"`
Name string `json:"name"`
}
type accountSourceScope struct {
OnlineOnly bool
AccountID string
Sources []string
}
type accountSourceContextKey struct{}
func allAccountSources() []string {
result := make([]string, 0, len(accountSourceChoices))
for _, choice := range accountSourceChoices {
result = append(result, choice.ID)
}
return result
}
func normalizeAccountSources(sources []string) ([]string, error) {
if sources == nil {
return nil, nil
}
if len(sources) == 0 || len(sources) > len(accountSourceChoices) {
return nil, errors.New("请至少选择一个可用站源")
}
selected := make(map[string]bool, len(sources))
for _, source := range sources {
valid := false
for _, choice := range accountSourceChoices {
if source == choice.ID {
valid = true
break
}
}
if !valid || selected[source] {
return nil, errors.New("站源权限无效,请重新选择")
}
selected[source] = true
}
result := make([]string, 0, len(sources))
for _, choice := range accountSourceChoices {
if selected[choice.ID] {
result = append(result, choice.ID)
}
}
return result, nil
}
func (account accountRecord) effectiveSources() []string {
if account.Admin || account.Sources == nil {
return allAccountSources()
}
return append([]string{}, account.Sources...)
}
func sourceScope(ctx context.Context) *accountSourceScope {
scope, _ := ctx.Value(accountSourceContextKey{}).(*accountSourceScope)
return scope
}
func withSourceScope(ctx context.Context, account accountRecord) context.Context {
return context.WithValue(ctx, accountSourceContextKey{}, &accountSourceScope{OnlineOnly: account.onlineOnly(), AccountID: account.ID, Sources: account.effectiveSources()})
}
func sourceScopeRestricted(ctx context.Context) bool {
scope := sourceScope(ctx)
return scope != nil && len(scope.Sources) < len(accountSourceChoices)
}
func accountSourceGroup(source string) string {
return accountSourceAliases[strings.ToLower(strings.TrimSpace(source))]
}
func accountDramaSource(id string) string {
id = strings.TrimSpace(id)
if source, sourceID, ok := splitProviderDramaID(id); ok && sourceID != "" {
return accountSourceGroup(source)
}
if id == "" || strings.Contains(id, ":") {
return ""
}
return accountLegacySource
}
func sourceAllowed(ctx context.Context, source string) bool {
scope := sourceScope(ctx)
if scope == nil {
return true
}
group := accountSourceGroup(source)
for _, allowed := range scope.Sources {
if allowed == group {
return true
}
}
return false
}
func dramaAllowed(ctx context.Context, id, source string) bool {
if sourceScope(ctx) == nil {
return true
}
group := accountDramaSource(id)
return group != "" && (source == "" || accountSourceGroup(source) == group) && sourceAllowed(ctx, group)
}
func taskSourceAllowed(ctx context.Context, task Task) bool {
return dramaAllowed(ctx, task.DramaID, task.Chapter.Source)
}
func requireSource(writer http.ResponseWriter, ctx context.Context, source string) bool {
if sourceAllowed(ctx, source) {
return true
}
writeSourceDenied(writer)
return false
}
func writeSourceDenied(writer http.ResponseWriter) {
writeViewerError(writer, http.StatusForbidden, "source_forbidden", "当前账号无权访问该站源,请联系管理员")
}
func (app *UIApp) requireDramaSources(writer http.ResponseWriter, request *http.Request, ids []string) bool {
app.mu.Lock()
allowed := app.dramaSourcesAllowedLocked(request.Context(), ids)
app.mu.Unlock()
if !allowed {
writeSourceDenied(writer)
}
return allowed
}
func (app *UIApp) dramaSourcesAllowedLocked(ctx context.Context, ids []string) bool {
wanted := make(map[string]bool, len(ids))
for _, id := range ids {
if !dramaAllowed(ctx, id, "") {
return false
}
wanted[id] = true
}
for _, drama := range app.dramas {
if wanted[drama.ID] && !dramaAllowed(ctx, drama.ID, drama.Source) {
return false
}
}
return true
}
func (app *UIApp) requireTaskSourcesLocked(writer http.ResponseWriter, request *http.Request, selection taskSelection) bool {
ctx := request.Context()
allowed := app.dramaSourcesAllowedLocked(ctx, selection.DramaIDs)
for _, id := range selection.IDs {
if task := app.tasks[id]; task != nil && (!dramaAllowed(ctx, task.DramaID, "") || !taskSourceAllowed(ctx, task.Source)) {
allowed = false
}
}
for _, id := range selection.idsLocked(app) {
if task := app.tasks[id]; task != nil && (!dramaAllowed(ctx, task.DramaID, "") || !taskSourceAllowed(ctx, task.Source)) {
allowed = false
}
}
if !allowed {
writeSourceDenied(writer)
}
return allowed
}
func (app *UIApp) taskViewsForSourceLocked(ctx context.Context) []uiTaskView {
views := app.taskViewsLocked()
if sourceScope(ctx) == nil {
return views
}
result := make([]uiTaskView, 0, len(views))
for _, view := range views {
task := app.tasks[view.ID]
if task != nil && dramaAllowed(ctx, view.DramaID, "") && taskSourceAllowed(ctx, task.Source) {
result = append(result, view)
}
}
return result
}
func (app *UIApp) librarySnapshotForSourceLocked(ctx context.Context, revision uint64) map[string]any {
if !sourceScopeRestricted(ctx) {
return app.librarySnapshotLocked(revision)
}
response := app.librarySnapshotLocked(0)
dramas := make([]Drama, 0)
for _, drama := range app.dramas {
if dramaAllowed(ctx, drama.ID, drama.Source) {
dramas = append(dramas, drama)
}
}
sources := map[string]librarySourceState{}
loading := false
for source, state := range app.librarySources {
if sourceAllowed(ctx, source) {
sources[source] = state
loading = loading || state.Status == "loading"
}
}
remaining := sortMetadataRemaining(dramas)
for source := range remaining {
if source != "" && !sourceAllowed(ctx, source) {
delete(remaining, source)
}
}
more := map[string]bool{}
for source, count := range remaining {
more[source] = count > 0
}
if sourceAllowed(ctx, sourceHongguo) {
more[sourceHongguo] = more[sourceHongguo] || hongguoCatalogHasMore(app.libraryApp)
}
for source, value := range more {
if source != "" {
more[""] = more[""] || value
}
}
response["data"], response["total"], response["sources"], response["error"] = dramas, len(dramas), sources, librarySourceErrors(sources)
response["loading"], response["loadingMore"] = loading, loading && app.libraryMore
response["loadingSource"] = ""
response["loadingPhase"] = ""
if sourceAllowed(ctx, app.libraryLoadingSource) {
response["loadingSource"] = app.libraryLoadingSource
response["loadingPhase"] = app.libraryLoadingPhase
}
response["metadataRemaining"], response["hasMoreBySource"], response["hasMore"] = remaining, more, more[""]
pending := 0
for _, drama := range dramas {
if app.metadataPending[drama.ID] {
pending++
}
}
response["metadata"] = map[string]any{"running": pending > 0, "checked": 0, "total": pending, "updated": 0, "failed": 0}
return response
}
func (app *UIApp) imageSource(ctx context.Context, remote string) (string, bool) {
source, _, allowed := app.imageTarget(ctx, "", remote)
return source, allowed
}
func (app *UIApp) imageTarget(ctx context.Context, id, remote string) (string, string, bool) {
app.mu.Lock()
defer app.mu.Unlock()
for _, drama := range app.dramas {
if id != "" && drama.ID != id || !dramaAllowed(ctx, drama.ID, drama.Source) {
continue
}
if address, valid := buildImageURL(bestDramaCover(drama)); valid && (id != "" || address == remote) {
return dramaProvider(drama), address, true
}
}
return "", "", false
}
func playbackSourceAllowed(ctx context.Context, session *playbackSession) bool {
if session == nil {
return false
}
if !downloadAllowed(ctx) && (len(session.downloadIDs) > 0 || session.accountID != "") {
return false
}
if session.dramaID != "" && !dramaAllowed(ctx, session.dramaID, "") {
return false
}
for _, task := range session.tasks {
if !taskSourceAllowed(ctx, task) {
return false
}
}
return true
}
func (app *UIApp) closeForbiddenPlaybacks(account accountRecord) {
ctx := withSourceScope(context.Background(), account)
var ids []string
app.playbackMu.Lock()
for id, session := range app.playbacks {
if (session.accountID == account.ID || session.viewer != nil && session.viewer.id == account.viewerID()) && !playbackSourceAllowed(ctx, session) {
ids = append(ids, id)
}
}
app.playbackMu.Unlock()
for _, id := range ids {
app.closePlayback(id)
}
}
func (app *UIApp) handleAdminAccountPermissions(writer http.ResponseWriter, request *http.Request) {
var input struct {
Username string `json:"username"`
Sources []string `json:"sources"`
OnlineOnly *bool `json:"onlineOnly"`
}
if !readAccountRequest(writer, request, &input) {
return
}
name, _, err := normalizeAccountUsername(input.Username)
sources, sourceErr := normalizeAccountSources(input.Sources)
if err != nil || sourceErr != nil || sources == nil && input.OnlineOnly == nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "请选择账号及需要修改的权限"})
return
}
store := app.browserViewers().accountStore()
store.mu.Lock()
account, exists := store.state.Accounts[name]
if !exists || account.Admin {
store.mu.Unlock()
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "只能修改现有普通账号的权限"})
return
}
if input.Sources != nil {
account.Sources = sources
}
if input.OnlineOnly != nil {
account.OnlineOnly = *input.OnlineOnly
}
state := store.snapshotLocked()
state.Accounts[name] = account
err = store.saveLocked(state)
store.mu.Unlock()
if err != nil {
accountOperationError(writer, err)
return
}
app.closeForbiddenPlaybacks(account)
writeJSON(writer, http.StatusOK, map[string]any{"ok": true, "account": account.public()})
}
func sourceRankingBoards(ctx context.Context) []rankingBoard {
result := make([]rankingBoard, 0, len(rankingBoards))
for _, board := range rankingBoards {
if sourceAllowed(ctx, board.Source) {
result = append(result, board)
}
}
return result
}
func (store *playbackHistoryStore) removeForSources(ctx context.Context, id string, all bool) error {
if !all || !sourceScopeRestricted(ctx) {
return store.remove(id, all)
}
store.mu.Lock()
if store.loadErr != nil {
err := store.loadErr
store.mu.Unlock()
return err
}
now := time.Now()
for id, entry := range store.entries {
if dramaAllowed(ctx, id, entry.Source) {
delete(store.entries, id)
store.deleted[id] = now
}
}
store.revision++
store.mu.Unlock()
return store.flush()
}
func (app *UIApp) embyAccountSourceAllowed(owner, dramaID string) bool {
store := app.browserViewers().accountStore()
store.mu.Lock()
defer store.mu.Unlock()
if store.err != nil {
return false
}
for _, account := range store.state.Accounts {
if account.ID == owner {
return !account.onlineOnly() && dramaAllowed(withSourceScope(context.Background(), account), dramaID, "")
}
}
return false
}