import { getDebridLinkApiKeyId, parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; import { getMegaDebridAccountId, getMegaDebridAccountsForMode, getMegaDebridCredentialsForMode, getMegaDebridDisabledAccountIdsForMode, mergeMegaDebridCredentialPools, parseMegaDebridAccounts, serializeMegaDebridAccounts, type MegaDebridAccountMode } from "../shared/mega-debrid-accounts"; import type { AccountCommand, AccountCredentialCheckInput, AppSettings, DebridProvider, RendererAccountKind } from "../shared/types"; export interface AppliedAccountCommand { settings: AppSettings; response: { accountId: string | null }; } const ACCOUNT_KINDS = new Set([ "realdebrid-api", "realdebrid-web", "megadebrid-api", "megadebrid-web", "bestdebrid-api", "bestdebrid-web", "alldebrid-api", "alldebrid-web", "ddownload-login", "onefichier-api", "debridlink-api", "linksnappy-login" ]); const ACTION_FIELDS: Record> = { create: new Set(["action", "kind", "identity", "secret", "dailyLimitBytes"]), replace: new Set(["action", "kind", "accountId", "identity", "secret", "dailyLimitBytes"]), "update-secret": new Set(["action", "kind", "accountId", "secret"]), delete: new Set(["action", "kind", "accountId"]) }; function invalid(): never { throw new Error("Account-Payload ist ungültig"); } function optionalString(value: unknown, maxLength: number): string | undefined { if (value === undefined) { return undefined; } if (typeof value !== "string" || value.length > maxLength) { invalid(); } return value; } function requiredString(value: unknown, maxLength: number): string { const result = optionalString(value, maxLength); if (result === undefined || !result.trim()) { invalid(); } return result; } function optionalLimit(value: unknown): number | undefined { if (value === undefined) { return undefined; } if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { invalid(); } return value; } export function validateAccountCommand(value: unknown): AccountCommand { if (!value || typeof value !== "object" || Array.isArray(value)) { invalid(); } const raw = value as Record; const action = raw.action; if (action !== "create" && action !== "replace" && action !== "update-secret" && action !== "delete") { invalid(); } if (Object.keys(raw).some((key) => !ACTION_FIELDS[action].has(key))) { invalid(); } if (typeof raw.kind !== "string" || !ACCOUNT_KINDS.has(raw.kind as RendererAccountKind)) { invalid(); } const kind = raw.kind as RendererAccountKind; if (action === "create") { return { action, kind, identity: optionalString(raw.identity, 512), secret: optionalString(raw.secret, 100_000), dailyLimitBytes: optionalLimit(raw.dailyLimitBytes) }; } const accountId = requiredString(raw.accountId, 256).trim(); if (action === "replace") { return { action, kind, accountId, identity: optionalString(raw.identity, 512), secret: optionalString(raw.secret, 100_000), dailyLimitBytes: optionalLimit(raw.dailyLimitBytes) }; } if (action === "update-secret") { return { action, kind, accountId, secret: requiredString(raw.secret, 100_000) }; } return { action, kind, accountId }; } export function validateAccountCredentialCheckInput(value: unknown): AccountCredentialCheckInput { if (!value || typeof value !== "object" || Array.isArray(value)) invalid(); const raw = value as Record; if (Object.keys(raw).some((key) => !new Set(["kind", "accountId", "identity", "secret"]).has(key))) invalid(); if (raw.kind !== "megadebrid-api" && raw.kind !== "megadebrid-web" && raw.kind !== "debridlink-api") invalid(); return { kind: raw.kind, accountId: optionalString(raw.accountId, 256), identity: optionalString(raw.identity, 512), secret: optionalString(raw.secret, 100_000) }; } function withoutKeys(record: Record, ...keys: string[]): Record { const removed = new Set(keys); return Object.fromEntries(Object.entries(record).filter(([key]) => !removed.has(key))); } function migrateIdList(values: readonly string[], oldId: string, newId: string): string[] { const retained = values.filter((value) => value !== oldId && value !== newId); if (values.includes(oldId)) { retained.push(newId); } return retained; } function setLimit(record: Record, id: string, value: number | undefined): Record { const result = { ...record }; if (value === undefined) { return result; } if (value > 0) { result[id] = value; } else { delete result[id]; } return result; } function migrateLimit(record: Record, oldId: string, newId: string, value: number | undefined): Record { const result = withoutKeys(record, oldId, newId); const resolved = value === undefined ? record[oldId] : value; if (resolved && resolved > 0) { result[newId] = resolved; } return result; } function validateIdentity(identity: string): string { const trimmed = identity.trim(); if (!trimmed || /[:\r\n]/.test(trimmed)) { invalid(); } return trimmed; } function validateSecret(secret: string): string { if (!secret.trim() || /[\r\n]/.test(secret)) { invalid(); } return secret; } function megaMode(kind: RendererAccountKind): MegaDebridAccountMode { return kind === "megadebrid-web" ? "web" : "api"; } function writeMegaPools(settings: AppSettings, apiCredentials: string, webCredentials: string): AppSettings { const mergedCredentials = mergeMegaDebridCredentialPools(apiCredentials, webCredentials); const first = parseMegaDebridAccounts(mergedCredentials)[0]; return { ...settings, megaCredentials: mergedCredentials, megaLogin: first?.login || "", megaPassword: first?.password || "", megaDebridApiCredentials: apiCredentials, megaDebridWebCredentials: webCredentials, megaDebridApiEnabled: settings.megaDebridApiEnabled && Boolean(apiCredentials), megaDebridWebEnabled: settings.megaDebridWebEnabled && Boolean(webCredentials) }; } function createMega(settings: AppSettings, command: Extract): AppliedAccountCommand { const mode = megaMode(command.kind); const identity = validateIdentity(command.identity || ""); const secret = validateSecret(command.secret || ""); const accounts = getMegaDebridAccountsForMode(settings, mode); if (accounts.some((account) => account.login.toLowerCase() === identity.toLowerCase())) { invalid(); } const selectedCredentials = serializeMegaDebridAccounts([...accounts, { login: identity, password: secret }]); const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api"); const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web"); const accountId = getMegaDebridAccountId(identity); const next = writeMegaPools(settings, apiCredentials, webCredentials); if (mode === "api") { next.megaDebridApiEnabled = true; } else { next.megaDebridWebEnabled = true; } next.megaDebridAccountDailyLimitBytes = setLimit(settings.megaDebridAccountDailyLimitBytes, accountId, command.dailyLimitBytes); return { settings: next, response: { accountId } }; } function replaceMega(settings: AppSettings, command: Extract): AppliedAccountCommand { const mode = megaMode(command.kind); const accounts = getMegaDebridAccountsForMode(settings, mode); const index = accounts.findIndex((account) => account.id === command.accountId); if (index < 0) { invalid(); } const current = accounts[index]; const identity = command.identity === undefined ? current.login : validateIdentity(command.identity); const secret = command.secret?.trim() ? validateSecret(command.secret) : current.password; if (accounts.some((account, accountIndex) => accountIndex !== index && account.login.toLowerCase() === identity.toLowerCase())) { invalid(); } const accountId = getMegaDebridAccountId(identity); const selectedCredentials = serializeMegaDebridAccounts(accounts.map((account, accountIndex) => accountIndex === index ? { login: identity, password: secret } : { login: account.login, password: account.password })); const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api"); const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web"); const next = writeMegaPools(settings, apiCredentials, webCredentials); const selectedDisabled = migrateIdList(getMegaDebridDisabledAccountIdsForMode(settings, mode), command.accountId, accountId); if (mode === "api") { next.megaDebridApiDisabledAccountIds = selectedDisabled; } else { next.megaDebridWebDisabledAccountIds = selectedDisabled; } next.megaDebridDisabledAccountIds = [...new Set([...next.megaDebridApiDisabledAccountIds, ...next.megaDebridWebDisabledAccountIds])]; next.megaDebridAccountDailyLimitBytes = migrateLimit(settings.megaDebridAccountDailyLimitBytes, command.accountId, accountId, command.dailyLimitBytes); if (command.accountId !== accountId) { next.megaDebridAccountDailyUsageBytes = withoutKeys(settings.megaDebridAccountDailyUsageBytes, command.accountId, accountId); next.megaDebridAccountTotalUsageBytes = withoutKeys(settings.megaDebridAccountTotalUsageBytes, command.accountId, accountId); next.debridAccountStatuses = withoutKeys(settings.debridAccountStatuses, command.accountId, accountId); } return { settings: next, response: { accountId } }; } function deleteMega(settings: AppSettings, command: Extract): AppliedAccountCommand { const mode = megaMode(command.kind); const selected = getMegaDebridAccountsForMode(settings, mode); if (!selected.some((account) => account.id === command.accountId)) { invalid(); } const selectedCredentials = serializeMegaDebridAccounts(selected.filter((account) => account.id !== command.accountId)); const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api"); const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web"); const accountStillUsed = parseMegaDebridAccounts(mode === "api" ? webCredentials : apiCredentials).some((account) => account.id === command.accountId); const next = writeMegaPools(settings, apiCredentials, webCredentials); next.megaDebridApiDisabledAccountIds = mode === "api" ? settings.megaDebridApiDisabledAccountIds.filter((id) => id !== command.accountId) : [...settings.megaDebridApiDisabledAccountIds]; next.megaDebridWebDisabledAccountIds = mode === "web" ? settings.megaDebridWebDisabledAccountIds.filter((id) => id !== command.accountId) : [...settings.megaDebridWebDisabledAccountIds]; next.megaDebridDisabledAccountIds = [...new Set([...next.megaDebridApiDisabledAccountIds, ...next.megaDebridWebDisabledAccountIds])]; if (!accountStillUsed) { next.megaDebridAccountDailyLimitBytes = withoutKeys(settings.megaDebridAccountDailyLimitBytes, command.accountId); next.megaDebridAccountDailyUsageBytes = withoutKeys(settings.megaDebridAccountDailyUsageBytes, command.accountId); next.megaDebridAccountTotalUsageBytes = withoutKeys(settings.megaDebridAccountTotalUsageBytes, command.accountId); next.debridAccountStatuses = withoutKeys(settings.debridAccountStatuses, command.accountId); } const remaining = getMegaDebridAccountsForMode(next, mode)[0]?.id || null; return { settings: next, response: { accountId: remaining } }; } function createDebridLink(settings: AppSettings, command: Extract): AppliedAccountCommand { const secret = validateSecret(command.secret || ""); if (/[,\r\n]/.test(secret)) { invalid(); } const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys); if (keys.some((key) => key.token === secret)) { invalid(); } const accountId = getDebridLinkApiKeyId(secret); return { settings: { ...settings, debridLinkApiKeys: [...keys.map((key) => key.token), secret].join("\n"), debridLinkApiKeyDailyLimitBytes: setLimit(settings.debridLinkApiKeyDailyLimitBytes, accountId, command.dailyLimitBytes) }, response: { accountId } }; } function replaceDebridLink(settings: AppSettings, command: Extract): AppliedAccountCommand { const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys); const index = keys.findIndex((key) => key.id === command.accountId); if (index < 0) { invalid(); } const secret = command.secret?.trim() ? validateSecret(command.secret) : keys[index].token; if (/[,\r\n]/.test(secret) || keys.some((key, keyIndex) => keyIndex !== index && key.token === secret)) { invalid(); } const accountId = getDebridLinkApiKeyId(secret); const tokens = keys.map((key, keyIndex) => keyIndex === index ? secret : key.token); const idChanged = accountId !== command.accountId; return { settings: { ...settings, debridLinkApiKeys: tokens.join("\n"), debridLinkDisabledKeyIds: idChanged ? migrateIdList(settings.debridLinkDisabledKeyIds, command.accountId, accountId) : [...settings.debridLinkDisabledKeyIds], debridLinkApiKeyDailyLimitBytes: migrateLimit(settings.debridLinkApiKeyDailyLimitBytes, command.accountId, accountId, command.dailyLimitBytes), debridLinkApiKeyDailyUsageBytes: idChanged ? withoutKeys(settings.debridLinkApiKeyDailyUsageBytes, command.accountId, accountId) : { ...settings.debridLinkApiKeyDailyUsageBytes }, debridLinkApiKeyTotalUsageBytes: idChanged ? withoutKeys(settings.debridLinkApiKeyTotalUsageBytes, command.accountId, accountId) : { ...settings.debridLinkApiKeyTotalUsageBytes }, debridAccountStatuses: idChanged ? withoutKeys(settings.debridAccountStatuses, command.accountId, accountId) : { ...settings.debridAccountStatuses } }, response: { accountId } }; } function deleteDebridLink(settings: AppSettings, command: Extract): AppliedAccountCommand { const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys); if (!keys.some((key) => key.id === command.accountId)) { invalid(); } const remaining = keys.filter((key) => key.id !== command.accountId); return { settings: { ...settings, debridLinkApiKeys: remaining.map((key) => key.token).join("\n"), debridLinkDisabledKeyIds: settings.debridLinkDisabledKeyIds.filter((id) => id !== command.accountId), debridLinkApiKeyDailyLimitBytes: withoutKeys(settings.debridLinkApiKeyDailyLimitBytes, command.accountId), debridLinkApiKeyDailyUsageBytes: withoutKeys(settings.debridLinkApiKeyDailyUsageBytes, command.accountId), debridLinkApiKeyTotalUsageBytes: withoutKeys(settings.debridLinkApiKeyTotalUsageBytes, command.accountId), debridAccountStatuses: withoutKeys(settings.debridAccountStatuses, command.accountId) }, response: { accountId: remaining[0]?.id || null } }; } function singleProvider(kind: RendererAccountKind): DebridProvider { if (kind.startsWith("realdebrid")) return "realdebrid"; if (kind.startsWith("bestdebrid")) return "bestdebrid"; if (kind.startsWith("alldebrid")) return "alldebrid"; if (kind === "ddownload-login") return "ddownload"; if (kind === "onefichier-api") return "onefichier"; if (kind === "linksnappy-login") return "linksnappy"; invalid(); } function singleConfigured(settings: AppSettings, kind: RendererAccountKind): boolean { if (kind === "realdebrid-api") return Boolean(settings.token.trim()); if (kind === "realdebrid-web") return settings.realDebridUseWebLogin; if (kind === "bestdebrid-api") return Boolean(settings.bestToken.trim()); if (kind === "bestdebrid-web") return settings.bestDebridUseWebLogin; if (kind === "alldebrid-api") return Boolean(settings.allDebridToken.trim()); if (kind === "alldebrid-web") return settings.allDebridUseWebLogin; if (kind === "ddownload-login") return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword); if (kind === "onefichier-api") return Boolean(settings.oneFichierApiKey.trim()); if (kind === "linksnappy-login") return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword); return false; } function setSingle(settings: AppSettings, kind: RendererAccountKind, identity: string | undefined, secret: string | undefined): AppSettings { if (kind === "realdebrid-api") return { ...settings, token: validateSecret(secret || ""), realDebridUseWebLogin: false }; if (kind === "realdebrid-web") return { ...settings, token: "", realDebridUseWebLogin: true }; if (kind === "bestdebrid-api") return { ...settings, bestToken: validateSecret(secret || ""), bestDebridUseWebLogin: false }; if (kind === "bestdebrid-web") return { ...settings, bestToken: "", bestDebridUseWebLogin: true }; if (kind === "alldebrid-api") return { ...settings, allDebridToken: validateSecret(secret || ""), allDebridUseWebLogin: false }; if (kind === "alldebrid-web") return { ...settings, allDebridToken: "", allDebridUseWebLogin: true }; if (kind === "ddownload-login") return { ...settings, ddownloadLogin: validateIdentity(identity || ""), ddownloadPassword: validateSecret(secret || "") }; if (kind === "onefichier-api") return { ...settings, oneFichierApiKey: validateSecret(secret || "") }; if (kind === "linksnappy-login") return { ...settings, linkSnappyLogin: validateIdentity(identity || ""), linkSnappyPassword: validateSecret(secret || "") }; invalid(); } function replaceSingle(settings: AppSettings, command: Extract): AppliedAccountCommand { if (!singleConfigured(settings, command.kind) || command.accountId !== `svc-${singleProvider(command.kind)}`) { invalid(); } let identity = command.identity; let secret = command.secret?.trim() ? command.secret : undefined; if (command.kind === "realdebrid-api") secret ||= settings.token; if (command.kind === "bestdebrid-api") secret ||= settings.bestToken; if (command.kind === "alldebrid-api") secret ||= settings.allDebridToken; if (command.kind === "ddownload-login") { identity = identity?.trim() ? identity : settings.ddownloadLogin; secret ||= settings.ddownloadPassword; } if (command.kind === "onefichier-api") secret ||= settings.oneFichierApiKey; if (command.kind === "linksnappy-login") { identity = identity?.trim() ? identity : settings.linkSnappyLogin; secret ||= settings.linkSnappyPassword; } const provider = singleProvider(command.kind); const next = setSingle(settings, command.kind, identity, secret); next.providerDailyLimitBytes = setLimit(settings.providerDailyLimitBytes as Record, provider, command.dailyLimitBytes); return { settings: next, response: { accountId: command.accountId } }; } function deleteSingle(settings: AppSettings, command: Extract): AppliedAccountCommand { const provider = singleProvider(command.kind); if (!singleConfigured(settings, command.kind) || command.accountId !== `svc-${provider}`) { invalid(); } let next = { ...settings }; if (provider === "realdebrid") next = { ...next, token: "", realDebridUseWebLogin: false }; if (provider === "bestdebrid") next = { ...next, bestToken: "", bestDebridUseWebLogin: false }; if (provider === "alldebrid") next = { ...next, allDebridToken: "", allDebridUseWebLogin: false }; if (provider === "ddownload") next = { ...next, ddownloadLogin: "", ddownloadPassword: "" }; if (provider === "onefichier") next = { ...next, oneFichierApiKey: "" }; if (provider === "linksnappy") next = { ...next, linkSnappyLogin: "", linkSnappyPassword: "" }; next.providerDailyLimitBytes = withoutKeys(settings.providerDailyLimitBytes as Record, provider); next.providerDailyUsageBytes = withoutKeys(settings.providerDailyUsageBytes as Record, provider); next.providerTotalUsageBytes = withoutKeys(settings.providerTotalUsageBytes as Record, provider); return { settings: next, response: { accountId: null } }; } function createSingle(settings: AppSettings, command: Extract): AppliedAccountCommand { if (singleConfigured(settings, command.kind)) { invalid(); } const provider = singleProvider(command.kind); const next = setSingle(settings, command.kind, command.identity, command.secret); next.providerDailyLimitBytes = setLimit(settings.providerDailyLimitBytes as Record, provider, command.dailyLimitBytes); return { settings: next, response: { accountId: `svc-${provider}` } }; } export function applyAccountCommand(settings: AppSettings, command: AccountCommand): AppliedAccountCommand { if (command.action === "update-secret") { const replace: Extract = { action: "replace", kind: command.kind, accountId: command.accountId, secret: command.secret }; return applyAccountCommand(settings, replace); } if (command.kind === "megadebrid-api" || command.kind === "megadebrid-web") { if (command.action === "create") return createMega(settings, command); if (command.action === "replace") return replaceMega(settings, command); return deleteMega(settings, command); } if (command.kind === "debridlink-api") { if (command.action === "create") return createDebridLink(settings, command); if (command.action === "replace") return replaceDebridLink(settings, command); return deleteDebridLink(settings, command); } if (command.action === "create") return createSingle(settings, command); if (command.action === "replace") return replaceSingle(settings, command); return deleteSingle(settings, command); }