api: move request functions to separate file

- request status id is no longer a cryptic number
- descriptive function names
This commit is contained in:
wukko 2024-05-15 21:39:44 +06:00
parent c10012130b
commit cc6345ff63
No known key found for this signature in database
GPG key ID: 3E30B3F26C7B4AA2
6 changed files with 183 additions and 131 deletions

View file

@ -6,14 +6,14 @@ const ipSalt = randomBytes(64).toString('hex');
import { env, version } from "../modules/config.js"; import { env, version } from "../modules/config.js";
import match from "../modules/processing/match.js"; import match from "../modules/processing/match.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js"; import { languageCode } from "../modules/sub/utils.js";
import { createResponse, verifyRequest, getIP } from "../modules/processing/request.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js";
import stream from "../modules/stream/stream.js"; import stream from "../modules/stream/stream.js";
import loc from "../localization/manager.js"; import loc from "../localization/manager.js";
import { generateHmac } from "../modules/sub/crypto.js"; import { generateHmac } from "../modules/sub/crypto.js";
import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; import { verifyStream, getInternalStream } from "../modules/stream/manage.js";
import { extract } from "../modules/processing/url.js"; import { extract } from "../modules/processing/url.js";
import { errorUnsupported } from "../modules/sub/errors.js";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) { export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = !env.corsWildcard ? { const corsConfig = !env.corsWildcard ? {
@ -100,7 +100,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const request = req.body; const request = req.body;
const lang = languageCode(req); const lang = languageCode(req);
const fail = (t) => { const fail = (t) => {
const { status, body } = apiJSON(0, { t: loc(lang, t) }); const { status, body } = createResponse("error", { t: loc(lang, t) });
res.status(status).json(body); res.status(status).json(body);
} }
@ -113,7 +113,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
} }
request.dubLang = request.dubLang ? lang : false; request.dubLang = request.dubLang ? lang : false;
const normalizedRequest = checkJSONPost(request); const normalizedRequest = verifyRequest(request);
if (!normalizedRequest) { if (!normalizedRequest) {
return fail('ErrorCantProcess'); return fail('ErrorCantProcess');
} }

View file

@ -1,6 +1,6 @@
import { strict as assert } from "node:assert"; import { strict as assert } from "node:assert";
import { apiJSON } from "../sub/utils.js"; import { createResponse } from "../processing/request.js";
import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js"; import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
@ -45,8 +45,8 @@ export default async function(host, patternMatch, lang, obj) {
try { try {
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata; let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) }); if (!testers[host]) return createResponse("error", { t: errorUnsupported(lang) });
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang, host) }); if (!(testers[host](patternMatch))) return createResponse("error", { t: brokenLink(lang, host) });
switch (host) { switch (host) {
case "twitter": case "twitter":
@ -177,17 +177,17 @@ export default async function(host, patternMatch, lang, obj) {
r = await dailymotion(patternMatch); r = await dailymotion(patternMatch);
break; break;
default: default:
return apiJSON(0, { t: errorUnsupported(lang) }); return createResponse("error", { t: errorUnsupported(lang) });
} }
if (r.isAudioOnly) isAudioOnly = true; if (r.isAudioOnly) isAudioOnly = true;
let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted; let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted;
if (r.error && r.critical) if (r.error && r.critical)
return apiJSON(6, { t: loc(lang, r.error) }) return createResponse("critical", { t: loc(lang, r.error) })
if (r.error) if (r.error)
return apiJSON(0, { return createResponse("error", {
t: Array.isArray(r.error) t: Array.isArray(r.error)
? loc(lang, r.error[0], r.error[1]) ? loc(lang, r.error[0], r.error[1])
: loc(lang, r.error) : loc(lang, r.error)
@ -199,7 +199,7 @@ export default async function(host, patternMatch, lang, obj) {
obj.filenamePattern, obj.twitterGif, obj.filenamePattern, obj.twitterGif,
requestIP requestIP
) )
} catch (e) { } catch {
return apiJSON(0, { t: genericError(lang, host) }) return createResponse("error", { t: genericError(lang, host) })
} }
} }

View file

@ -1,11 +1,11 @@
import { audioIgnore, services, supportedAudio } from "../config.js"; import { audioIgnore, services, supportedAudio } from "../config.js";
import { apiJSON } from "../sub/utils.js"; import { createResponse } from "../processing/request.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js"; import createFilename from "./createFilename.js";
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif, requestIP) { export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif, requestIP) {
let action, let action,
responseType = 2, responseType = "stream",
defaultParams = { defaultParams = {
u: r.urls, u: r.urls,
service: host, service: host,
@ -36,10 +36,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
switch (action) { switch (action) {
default: default:
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); return createResponse("error", { t: loc(lang, 'ErrorEmptyDownload') });
case "photo": case "photo":
responseType = 1; responseType = "redirect";
break; break;
case "gif": case "gif":
@ -56,11 +56,12 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
u: Array.isArray(r.urls) ? r.urls[0] : r.urls, u: Array.isArray(r.urls) ? r.urls[0] : r.urls,
mute: true mute: true
} }
if (host === "reddit" && r.typeId === 1) responseType = 1; if (host === "reddit" && r.typeId === "redirect")
responseType = "redirect";
break; break;
case "picker": case "picker":
responseType = 5; responseType = "picker";
switch (host) { switch (host) {
case "instagram": case "instagram":
case "twitter": case "twitter":
@ -98,7 +99,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
if (Array.isArray(r.urls)) { if (Array.isArray(r.urls)) {
params = { type: "render" } params = { type: "render" }
} else { } else {
responseType = 1; responseType = "redirect";
} }
break; break;
@ -106,7 +107,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
if (r.type === "remux") { if (r.type === "remux") {
params = { type: r.type }; params = { type: r.type };
} else { } else {
responseType = 1; responseType = "redirect";
} }
break; break;
@ -121,14 +122,15 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
case "tumblr": case "tumblr":
case "pinterest": case "pinterest":
case "streamable": case "streamable":
responseType = 1; responseType = "redirect";
break; break;
} }
break; break;
case "audio": case "audio":
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) { if (audioIgnore.includes(host)
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }) || (host === "reddit" && r.typeId === "redirect")) {
return createResponse("error", { t: loc(lang, 'ErrorEmptyDownload') })
} }
let processType = "render", let processType = "render",
@ -178,5 +180,5 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
break; break;
} }
return apiJSON(responseType, {...defaultParams, ...params}) return createResponse(responseType, {...defaultParams, ...params})
} }

View file

@ -0,0 +1,154 @@
import ipaddr from "ipaddr.js";
import { normalizeURL } from "../processing/url.js";
import { createStream } from "../stream/manage.js";
const apiVar = {
allowed: {
vCodec: ["h264", "av1", "vp9"],
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
},
booleanOnly: [
"isAudioOnly",
"isTTFullAudio",
"isAudioMuted",
"dubLang",
"disableMetadata",
"twitterGif",
"tiktokH265"
]
}
export function createResponse(responseType, responseData) {
try {
let status = 200,
response = {};
switch(responseType) {
case "error":
status = 400;
break;
case "rate-limit":
status = 429;
break;
}
switch (responseType) {
case "error":
case "success":
case "rate-limit":
response = {
text: responseData.t
}
break;
case "redirect":
response = {
url: responseData.u
}
break;
case "stream":
response = {
url: createStream(responseData)
}
break;
case "picker":
let pickerType = "various",
audio = false;
if (responseData.service === "tiktok") {
audio = responseData.u
pickerType = "images"
}
response = {
pickerType: pickerType,
picker: responseData.picker,
audio: audio
}
break;
default:
throw "unreachable"
}
return {
status,
body: {
status: responseType,
...response
}
}
} catch {
return {
status: 500,
body: {
status: "error",
text: "Internal Server Error"
}
}
}
}
export function verifyRequest(request) {
try {
let template = {
url: normalizeURL(decodeURIComponent(request.url)),
vCodec: "h264",
vQuality: "720",
aFormat: "mp3",
filenamePattern: "classic",
isAudioOnly: false,
isTTFullAudio: false,
isAudioMuted: false,
disableMetadata: false,
dubLang: false,
twitterGif: false,
tiktokH265: false
}
const requestKeys = Object.keys(request);
const templateKeys = Object.keys(template);
if (requestKeys.length > templateKeys.length + 1 || !request.url) {
return false;
}
for (const i in requestKeys) {
const key = requestKeys[i];
const item = request[key];
if (String(key) !== "url" && templateKeys.includes(key)) {
if (apiVar.booleanOnly.includes(key)) {
template[key] = !!item;
} else if (apiVar.allowed[key] && apiVar.allowed[key].includes(item)) {
template[key] = String(item)
}
}
}
if (template.dubLang)
template.dubLang = verifyLanguageCode(request.dubLang);
return template
} catch {
return false
}
}
export function getIP(req) {
const strippedIP = req.ip.replace(/^::ffff:/, '');
const ip = ipaddr.parse(strippedIP);
if (ip.kind() === 'ipv4') {
return strippedIP;
}
const prefix = 56;
const v6Bytes = ip.toByteArray();
v6Bytes.fill(0, prefix / 8);
return ipaddr.fromByteArray(v6Bytes).toString();
}

View file

@ -68,7 +68,7 @@ export default async function(obj) {
data = data[0]?.data?.children[0]?.data; data = data[0]?.data?.children[0]?.data;
if (data?.url?.endsWith('.gif')) return { if (data?.url?.endsWith('.gif')) return {
typeId: 1, typeId: "redirect",
urls: data.url urls: data.url
} }
@ -106,12 +106,12 @@ export default async function(obj) {
let id = video.split('/')[3]; let id = video.split('/')[3];
if (!audio) return { if (!audio) return {
typeId: 1, typeId: "redirect",
urls: video urls: video
} }
return { return {
typeId: 2, typeId: "stream",
type: "render", type: "render",
urls: [video, audioFileLink], urls: [video, audioFileLink],
audioFilename: `reddit_${id}_audio`, audioFilename: `reddit_${id}_audio`,

View file

@ -1,58 +1,5 @@
import { normalizeURL } from "../processing/url.js";
import { createStream } from "../stream/manage.js";
import ipaddr from "ipaddr.js";
const apiVar = {
allowed: {
vCodec: ["h264", "av1", "vp9"],
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
},
booleanOnly: [
"isAudioOnly",
"isTTFullAudio",
"isAudioMuted",
"dubLang",
"disableMetadata",
"twitterGif",
"tiktokH265"
]
}
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
export function apiJSON(type, obj) {
try {
switch (type) {
case 0:
return { status: 400, body: { status: "error", text: obj.t } };
case 1:
return { status: 200, body: { status: "redirect", url: obj.u } };
case 2:
return { status: 200, body: { status: "stream", url: createStream(obj) } };
case 3:
return { status: 200, body: { status: "success", text: obj.t } };
case 4:
return { status: 429, body: { status: "rate-limit", text: obj.t } };
case 5:
let pickerType = "various", audio = false
switch (obj.service) {
case "douyin":
case "tiktok":
audio = obj.u
pickerType = "images"
break;
}
return { status: 200, body: { status: "picker", pickerType: pickerType, picker: obj.picker, audio: audio } };
case 6: // critical error, action should be taken by balancer/other server software
return { status: 500, body: { status: "error", text: obj.t, critical: true } };
default:
return { status: 400, body: { status: "error", text: "Bad Request" } };
}
} catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error", critical: true } };
}
}
export function metadataManager(obj) { export function metadataManager(obj) {
let keys = Object.keys(obj); let keys = Object.keys(obj);
let tags = ["album", "composer", "genre", "copyright", "encoded_by", "title", "language", "artist", "album_artist", "performer", "disc", "publisher", "track", "encoder", "compilation", "date", "creation_time", "comment"] let tags = ["album", "composer", "genre", "copyright", "encoded_by", "title", "language", "artist", "album_artist", "performer", "disc", "publisher", "track", "encoder", "compilation", "date", "creation_time", "comment"]
@ -79,57 +26,6 @@ export function unicodeDecode(str) {
return String.fromCharCode(parseInt(unicode.replace(/\\u/g, ""), 16)); return String.fromCharCode(parseInt(unicode.replace(/\\u/g, ""), 16));
}); });
} }
export function checkJSONPost(obj) {
let def = {
url: normalizeURL(decodeURIComponent(obj.url)),
vCodec: "h264",
vQuality: "720",
aFormat: "mp3",
filenamePattern: "classic",
isAudioOnly: false,
isTTFullAudio: false,
isAudioMuted: false,
disableMetadata: false,
dubLang: false,
twitterGif: false,
tiktokH265: false
}
try {
let objKeys = Object.keys(obj);
let defKeys = Object.keys(def);
if (objKeys.length > defKeys.length + 1 || !obj.url) return false;
for (let i in objKeys) {
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else {
if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
}
}
}
if (def.dubLang)
def.dubLang = verifyLanguageCode(obj.dubLang);
return def
} catch (e) {
return false
}
}
export function getIP(req) {
const strippedIP = req.ip.replace(/^::ffff:/, '');
const ip = ipaddr.parse(strippedIP);
if (ip.kind() === 'ipv4') {
return strippedIP;
}
const prefix = 56;
const v6Bytes = ip.toByteArray();
v6Bytes.fill(0, prefix / 8);
return ipaddr.fromByteArray(v6Bytes).toString();
}
export function cleanHTML(html) { export function cleanHTML(html) {
let clean = html.replace(/ {4}/g, ''); let clean = html.replace(/ {4}/g, '');
clean = clean.replace(/\n/g, ''); clean = clean.replace(/\n/g, '');