From 27b2ef9bd6a1f6ad4bc3c8aadf1bcd205057ea43 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Thu, 26 Dec 2024 19:13:58 +0000 Subject: [PATCH] feat: move config validator into addon package and also check config at each request --- packages/addon/src/addon.ts | 15 - packages/addon/src/config.ts | 282 +++++++++++++++ packages/addon/src/index.ts | 1 + packages/addon/src/server.ts | 16 +- packages/frontend/src/app/configure/page.tsx | 357 +------------------ packages/frontend/tsconfig.json | 37 +- packages/types/src/types.ts | 13 + 7 files changed, 339 insertions(+), 382 deletions(-) create mode 100644 packages/addon/src/config.ts diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index 3288f54a..27c5f362 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -9,21 +9,6 @@ export class AIOStreams { this.config = config; } - public configValidator() { - if (!this.config) { - throw new Error('No config provided'); - } - if (!this.config.resolutions) { - throw new Error('No resolutions provided'); - } - if (!this.config.qualities) { - throw new Error('No qualities provided'); - } - if (!this.config.visualTags) { - throw new Error('No visualTags provided'); - } - } - public async getStreams(streamRequest: StreamRequest): Promise { const streams: Stream[] = []; diff --git a/packages/addon/src/config.ts b/packages/addon/src/config.ts new file mode 100644 index 00000000..e3015a6d --- /dev/null +++ b/packages/addon/src/config.ts @@ -0,0 +1,282 @@ +import { Config } from "@aiostreams/types"; +import { AddonDetail } from "@aiostreams/types"; + + +export const allowedFormatters = ['gdrive', 'torrentio', 'torbox']; + + +export const addonDetails: AddonDetail[] = [ + { + name: 'Torrentio', + id: 'torrentio', + options: [ + { + id: 'overrideUrl', + required: false, + + label: 'Override URL', + description: + 'Override the URL used to fetch streams from the torrentio addon', + type: 'text', + }, + { + id: 'useMultipleInstances', + required: false, + + label: 'Use Multiple Instances', + description: + 'Use multiple instances of the torrentio addon to fetch streams when using multiple services', + type: 'checkbox', + }, + ], + }, + { + name: 'Torbox', + id: 'torbox', + }, + { + name: 'Google Drive (Viren070)', + id: 'gdrive', + options: [ + { + id: 'addonUrl', + required: true, + label: 'Addon URL', + description: 'The URL of the Google Drive addon', + type: 'text', + }, + ], + }, + { + name: 'Custom', + id: 'custom', + options: [ + { + id: 'url', + required: true, + description: 'The URL of the custom addon', + label: 'URL', + type: 'text', + }, + { + id: 'name', + required: true, + description: 'The name of the custom addon', + label: 'Name', + type: 'text', + } + ], + }, +]; + + +export const allowedLanguages = ['English', 'Spanish', 'French', 'German', 'Chinese']; + +export const serviceCredentials = [ + { + name: 'Real Debrid', + id: 'realdebrid', + credentials: [ + { + label: 'API Key', + id: 'apiKey', + link: 'https://real-debrid.com/apitoken', + } + ] + }, + { + name: 'All Debrid', + id: 'alldebrid', + credentials: [ + { + label: 'API Key', + id: 'apiKey', + link: 'https://alldebrid.com/apikeys', + } + ] + }, + { + name: 'Premiumize', + id: 'premiumize', + credentials: [ + { + label: 'API Key', + id: 'apiKey', + link: 'https://www.premiumize.me/account', + } + ] + }, + { + name: 'Debrid Link', + id: 'debridlink', + credentials: [ + { + label: 'API Key', + id: 'apiKey', + link: 'https://debrid-link.com/webapp/apikey', + } + ] + }, + { + name: 'Torbox', + id: 'torbox', + credentials: [ + { + label: 'API Key', + id: 'apiKey', + link: 'https://torbox.app/settings', + } + ] + }, + { + name: 'Offcloud', + id: 'offcloud', + credentials: [ + { + label: 'API Key', + id: 'apiKey', + link: 'https://offcloud.com/#/account', + } + ] + }, + { + name: 'put.io', + id: 'putio', + credentials: [ + { + label: 'Client ID', + id: 'clientId', + link: 'https://put.io/oauth', + }, + { + label: 'Token', + id: 'token', + link: 'https://put.io/oauth', + } + ] + }, + { + name: 'Easynews', + id: 'easynews', + credentials: [ + { + label: 'Username', + id: 'username', + link: 'https://www.easynews.com/', + }, + { + label: 'Password', + id: 'password', + link: 'https://www.easynews.com/', + } + ] + }, +] + +export function validateConfig(config: Config): { valid: boolean; errorCode: string | null; errorMessage: string | null } { + + const createResponse = (valid: boolean, errorCode: string | null, errorMessage: string | null) => { + return { valid, errorCode, errorMessage }; + } + + // check for any duplicate addons where both the ID and options are the same + const duplicateAddons = config.addons.filter( + (addon, index) => + config.addons.findIndex( + (a) => + a.id === addon.id && + JSON.stringify(a.options) === JSON.stringify(addon.options) + ) !== index + ); + + if (duplicateAddons.length > 0) { + return createResponse(false, 'duplicateAddons', 'Duplicate addons found. Please remove any duplicates'); + } + + + for (const addon of config.addons) { + // if torbox addon is enabled, torbox service must be enabled and torbox api key must be set + if (addon.id === 'torbox') { + const torboxService = config.services.find( + (service) => service.id === 'torbox' + ); + if (!torboxService) { + return createResponse(false, 'torboxServiceNotEnabled', 'Torbox service must be enabled to use the Torbox addon'); + } + if (!torboxService.credentials.apiKey) { + return createResponse(false, 'torboxApiKeyNotSet', 'Torbox API Key must be set to use the Torbox addon'); + + } + } + const details = addonDetails.find((detail) => detail.id === addon.id); + if (!details) { + return createResponse(false, 'invalidAddon', `Invalid addon: ${addon.id}`); + } + if (details.options) { + for (const option of details.options) { + if (option.required && !addon.options[option.id]) { + return createResponse(false, 'missingRequiredOption', `Option ${option.label} is required for addon ${addon.id}`); + } + + if (option.id.toLowerCase().includes('url') && addon.options[option.id]) { + console.log('checking url', addon.options[option.id]); + try { + new URL(addon.options[option.id]); + } catch (_) { + return createResponse(false, 'invalidUrl', `Invalid URL for ${option.label}`); + } + } + } + } + } + + if (!allowedFormatters.includes(config.formatter)) { + return createResponse(false, 'invalidFormatter', `Invalid formatter: ${config.formatter}`); + } + + for (const service of config.services) { + if (service.enabled) { + const serviceDetail = serviceCredentials.find( + (detail) => detail.id === service.id + ); + if (!serviceDetail) { + return createResponse(false, 'invalidService', `Invalid service: ${service.id}`); + + } + for (const credential of serviceDetail.credentials) { + if (!service.credentials[credential.id]) { + return createResponse(false, 'missingCredential', `${credential.label} is required for ${service.name}`); + } + } + } + } + + // need at least one visual tag, resolution, quality + if (config.visualTags.length === 0) { + return createResponse(false, 'noVisualTags', 'At least one visual tag must be selected'); + } + + if (config.resolutions.length === 0) { + return createResponse(false, 'noResolutions', 'At least one resolution must be selected'); + } + + if (config.qualities.length === 0) { + return createResponse(false, 'noQualities', 'At least one quality must be selected'); + } + + if (config.minSize && config.maxSize) { + if (config.minSize >= config.maxSize) { + return createResponse(false, 'invalidSizeRange', 'Your minimum size limit can\'t be greater than or equal to your maximum size limit'); + } + } + + if (config.addons.length < 1) { + return createResponse(false, 'noAddons', 'At least one addon must be selected'); + } + + if (config.addons.length > 10) { + return createResponse(false, 'tooManyAddons', 'You can only select a maximum of 10 addons'); + } + + return createResponse(true, null, null); + }; \ No newline at end of file diff --git a/packages/addon/src/index.ts b/packages/addon/src/index.ts index 9762837e..5b7735b8 100644 --- a/packages/addon/src/index.ts +++ b/packages/addon/src/index.ts @@ -1 +1,2 @@ export * from './addon'; +export * from './config'; \ No newline at end of file diff --git a/packages/addon/src/server.ts b/packages/addon/src/server.ts index c7163f3c..cb4e09b3 100644 --- a/packages/addon/src/server.ts +++ b/packages/addon/src/server.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express'; import path from 'path'; import { AIOStreams } from './addon'; import { Config, StreamRequest } from '@aiostreams/types'; +import { validateConfig } from './config'; import { version, description } from '../package.json'; @@ -135,24 +136,21 @@ app.get('/:config/stream/:type/:id', (req: Request, res: Response) => { return; } try { - const aioStreams = new AIOStreams(configJson); - - try { - aioStreams.configValidator(); - } catch (error: any) { - console.log(`Invalid config: ${error.message}`); + const { valid, errorCode, errorMessage } = validateConfig(configJson); + if (!valid) { + console.error(`Invalid config: ${errorCode} - ${errorMessage}`); res.status(200).json({ streams: [ { url: 'https://example.com', name: 'Invalid Config', - description: error.message, + description: errorMessage, }, - ], + ] }) - return; } + const aioStreams = new AIOStreams(configJson); aioStreams.getStreams({ id, type, season, episode }).then((streams) => { res.status(200).json({ streams: streams }); }); diff --git a/packages/frontend/src/app/configure/page.tsx b/packages/frontend/src/app/configure/page.tsx index 1f1449bd..10e10aba 100644 --- a/packages/frontend/src/app/configure/page.tsx +++ b/packages/frontend/src/app/configure/page.tsx @@ -14,6 +14,7 @@ import ServiceInput from '../../components/ServiceInput'; import AddonsList from '../../components/AddonsList'; import { Slide, ToastContainer, toast } from 'react-toastify'; import addonPackage from '../../../package.json'; +import { allowedFormatters, allowedLanguages, addonDetails, validateConfig, serviceCredentials } from '@aiostreams/config'; const version = addonPackage.version; @@ -51,86 +52,6 @@ const defaultSortCriteria: SortBy[] = [ { seeders: false }, ]; -const allowedFormatters = ['gdrive', 'torrentio', 'torbox']; - -interface AddonDetail { - name: string; - id: string; - options?: { - id: string; - required?: boolean; - label: string; - description?: string; - type: 'text' | 'checkbox'; - }[]; -} - -const addonDetails: AddonDetail[] = [ - { - name: 'Torrentio', - id: 'torrentio', - options: [ - { - id: 'overrideUrl', - required: false, - - label: 'Override URL', - description: - 'Override the URL used to fetch streams from the torrentio addon', - type: 'text', - }, - { - id: 'useMultipleInstances', - required: false, - - label: 'Use Multiple Instances', - description: - 'Use multiple instances of the torrentio addon to fetch streams when using multiple services', - type: 'checkbox', - }, - ], - }, - { - name: 'Torbox', - id: 'torbox', - }, - { - name: 'Google Drive (Viren070)', - id: 'gdrive', - options: [ - { - id: 'addonUrl', - required: true, - label: 'Addon URL', - description: 'The URL of the Google Drive addon', - type: 'text', - }, - ], - }, - { - name: 'Custom', - id: 'custom', - options: [ - { - id: 'url', - required: true, - description: 'The URL of the custom addon', - label: 'URL', - type: 'text', - }, - { - id: 'name', - required: true, - description: 'The name of the custom addon', - label: 'Name', - type: 'text', - } - ], - }, -]; - -const allowedLanguages = ['English', 'Spanish', 'French', 'German', 'Chinese']; - function showToast( message: string, type: 'success' | 'error' | 'info' | 'warning', @@ -219,106 +140,7 @@ const defaultServices = [ ]; -const serviceCredentials = [ - { - name: 'Real Debrid', - id: 'realdebrid', - credentials: [ - { - label: 'API Key', - id: 'apiKey', - link: 'https://real-debrid.com/apitoken', - } - ] - }, - { - name: 'All Debrid', - id: 'alldebrid', - credentials: [ - { - label: 'API Key', - id: 'apiKey', - link: 'https://alldebrid.com/api', - } - ] - }, - { - name: 'Premiumize', - id: 'premiumize', - credentials: [ - { - label: 'API Key', - id: 'apiKey', - link: 'https://www.premiumize.me/account', - } - ] - }, - { - name: 'Debrid Link', - id: 'debridlink', - credentials: [ - { - label: 'API Key', - id: 'apiKey', - link: 'https://debrid-link.com/webapp/apikey', - } - ] - }, - { - name: 'Torbox', - id: 'torbox', - credentials: [ - { - label: 'API Key', - id: 'apiKey', - link: 'https://torbox.app/settings', - } - ] - }, - { - name: 'Offcloud', - id: 'offcloud', - credentials: [ - { - label: 'API Key', - id: 'apiKey', - link: 'https://offcloud.com/#/account', - } - ] - }, - { - name: 'put.io', - id: 'putio', - credentials: [ - { - label: 'Client ID', - id: 'clientId', - link: 'https://put.io/oauth', - }, - { - label: 'Token', - id: 'token', - link: 'https://put.io/oauth', - } - ] - }, - { - name: 'Easynews', - id: 'easynews', - credentials: [ - { - label: 'Username', - id: 'username', - link: 'https://www.easynews.com/', - }, - { - label: 'Password', - id: 'password', - link: 'https://www.easynews.com/', - } - ] - }, -] + export default function Configure() { const [resolutions, setResolutions] = @@ -382,180 +204,19 @@ export default function Configure() { return `${protocol}//${root}/${encodedConfig}/manifest.json`; }; - const validateConfig = () => { + const createAndValidateConfig = () => { const config = createConfig(); - // check for any duplicate addons where both the ID and options are the same - const duplicateAddons = config.addons.filter( - (addon, index) => - config.addons.findIndex( - (a) => - a.id === addon.id && - JSON.stringify(a.options) === JSON.stringify(addon.options) - ) !== index - ); - - if (duplicateAddons.length > 0) { - showToast( - 'Duplicate addons found. Please remove any duplicates', - 'error', - 'duplicateAddons' - ); - return false - } - - - for (const addon of config.addons) { - // if torbox addon is enabled, torbox service must be enabled and torbox api key must be set - if (addon.id === 'torbox') { - const torboxService = config.services.find( - (service) => service.id === 'torbox' - ); - if (!torboxService) { - showToast( - 'Torbox service must be enabled to use the Torbox addon', - 'error', - 'torboxServiceNotEnabled' - ); - return false; - } - if (!torboxService.credentials.apiKey) { - showToast( - 'Torbox API Key must be set to use the Torbox addon', - 'error', - 'torboxApiKeyNotSet' - ); - return false; - } - } - const details = addonDetails.find((detail) => detail.id === addon.id); - if (!details) { - showToast(`Invalid addon: ${addon.id}`, 'error', 'invalidAddon'); - return false; - } - if (details.options) { - for (const option of details.options) { - if (option.required && !addon.options[option.id]) { - showToast( - `Option ${option.label} is required for addon ${addon.id}`, - 'error', - 'missingRequiredOption' - ); - return false; - } - console.log(option.label); - if (option.id.toLowerCase().includes('url')) { - try { - new URL(addon.options[option.id]); - } catch (_) { - showToast( - `Invalid URL for ${option.label}`, - 'error', - 'invalidUrl' - ); - return false; - } - } - } - } - } - - if (!allowedFormatters.includes(config.formatter)) { - showToast( - `Invalid formatter: ${config.formatter}`, - 'error', - 'invalidFormatter' - ); + const { valid, errorCode, errorMessage } = validateConfig(config); + if (!valid) { + showToast(errorMessage || 'Invalid config', 'error', errorCode || 'error'); return false; } - - for (const service of config.services) { - if (service.enabled) { - const serviceDetail = serviceCredentials.find( - (detail) => detail.id === service.id - ); - if (!serviceDetail) { - showToast(`Invalid service: ${service.id}`, 'error', 'invalidService'); - return false; - } - for (const credential of serviceDetail.credentials) { - if (!service.credentials[credential.id]) { - showToast( - `${credential.label} is required for ${service.name}`, - 'error', - `missing${service.id}${credential.id}` - ); - return false; - } - } - } - } - - // need at least one visual tag, resolution, quality - if (config.visualTags.length === 0) { - showToast( - 'At least one visual tag must be selected', - 'error', - 'noVisualTags' - ); - return false; - } - - if (config.resolutions.length === 0) { - showToast( - 'At least one resolution must be selected', - 'error', - 'noResolutions' - ); - return false; - } - - if (config.qualities.length === 0) { - showToast( - 'At least one quality must be selected', - 'error', - 'noQualities' - ); - return false; - } - - if (config.minSize && config.maxSize) { - if (config.minSize > config.maxSize) { - showToast( - "Your minimum size limit can't be greater than your maximum size limit", - 'error', - 'invalidSizeRange' - ); - return false; - } else if (config.minSize === config.maxSize) { - setTimeout(() => { - showToast( - 'Your minimum and maximum size are the same, this will result in no streams being shown', - 'warning', - 'sameSize' - ); - }, 500); - } - } - - if (config.addons.length < 1) { - showToast('At least one addon must be selected', 'error', 'noAddons'); - return false; - } - - if (config.addons.length > 10) { - showToast( - 'You can only select a maximum of 10 addons', - 'error', - 'tooManyAddons' - ); - } - return true; }; const handleInstall = () => { - if (validateConfig()) { + if (createAndValidateConfig()) { const manifestUrl = getManifestUrl(); const stremioUrl = manifestUrl.replace(/^https?/, 'stremio'); showToast( @@ -571,7 +232,7 @@ export default function Configure() { }; const handleInstallToWeb = () => { - if (validateConfig()) { + if (createAndValidateConfig()) { const manifestUrl = getManifestUrl(); const encodedManifestUrl = encodeURIComponent(manifestUrl); showToast( @@ -590,7 +251,7 @@ export default function Configure() { }; const handleCopyLink = () => { - if (validateConfig()) { + if (createAndValidateConfig()) { const manifestUrl = getManifestUrl(); navigator.clipboard.writeText(manifestUrl).then(() => { showToast( diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index 25ab25b2..2c007b37 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -1,17 +1,17 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, - "strict": true, - "noEmit": false, - "composite": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, - "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ @@ -20,14 +20,31 @@ } ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ], + "@aiostreams/config": [ + "../addon/src/config" + ] + }, + "noEmit": false, + "isolatedModules": true }, "references": [ { "path": "../types" + }, + { + "path": "../addon" } ], - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index cd6d008c..a3252143 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -113,3 +113,16 @@ export interface Config { credentials: { [key: string]: string }; }[]; } + +export interface AddonDetail { + name: string; + id: string; + options?: { + id: string; + required?: boolean; + label: string; + description?: string; + type: 'text' | 'checkbox'; + }[]; +} +