FlareSolverr.js/src/FlareSolverr.ts

244 lines
8.5 KiB
TypeScript

import http from 'node:http';
import {
BadRequestError,
InternalEndpointError,
UnknownError,
} from './Errors.js';
import {
IOptions,
IBaseAnswer,
TPostData,
ISessionsAnswer,
ISessionCreateOptions,
ISessionAnswer,
IRequestGetOptions,
IRequestPostOptions,
IRequestAnswer,
IRawRequestAnswer,
} from './Interfaces.js';
export class FlareSolverr {
private readonly endpoint: URL;
/**
* Default settings
* @alpha
* @internal
*/
private readonly options: IOptions = {
limit: 3,
};
constructor(endpoint: string | URL, options?: Partial<IOptions>) {
this.endpoint = new URL(endpoint.toString());
Object.assign(this.options, options);
}
/**
* Makes a request and returns a response object of type T.
* @param data - Data to send
* @returns A Promise that resolves to the response object of type T
*
* @throws InternalEndpointError - If there is an error on the internal server
* @throws UnknownError - If there is an unknown error
* @throws BadRequestError - If the response status is 'error'
* @throws Error - If any node.js errors happened
*
* @internal
*/
private _makeRequest<T extends IBaseAnswer>(data: TPostData): Promise<T> {
return new Promise<T>((resolve, reject) => {
let _resp_data = '';
const resp = http.request(
this.endpoint,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
(resp) => {
resp.setEncoding('utf8');
resp.on('data', (chunk) => (_resp_data += chunk));
resp.on('error', (error) => {
reject(error);
});
resp.on('end', () => {
switch (resp.statusCode) {
case 500: {
try {
const answer = JSON.parse(_resp_data) as {
error?: string;
} & IBaseAnswer;
if (answer.error) {
reject(
new InternalEndpointError(
answer.error,
),
);
} else {
if (answer.status !== undefined) {
reject(new BadRequestError(answer));
}
}
} finally {
reject(
new InternalEndpointError(
'Unknown error',
),
);
}
break;
}
case 200: {
resolve(JSON.parse(_resp_data) as T);
break;
}
default: {
reject(
new InternalEndpointError(
`Unknown error, ${
resp.statusCode ?? 'XXX'
} response code`,
),
);
break;
}
}
});
},
);
resp.on('error', (error_) => {
if (Object.keys(error_).includes('code')) {
const code = (error_ as Error & { code: string }).code;
const error = new UnknownError();
switch (code) {
case 'ECONNREFUSED': {
error.message = 'Is there correct endpoint url?';
break;
}
}
if (error.stack) {
error.stack = error.stack.replace(
'\n ',
'\n ' +
'Original error message:' +
error_.message +
'\n ',
);
}
reject(error);
} else {
reject(error_);
}
});
resp.end(JSON.stringify(data));
});
}
/**
* Retrieves a list of current sessions.
* @remarks All possible errors you can see in the {@link FlareSolverr._makeRequest | _makeRequest} method
* @returns Promise that is resolved to an object of type ISessionsAnswer.
*/
async getSessions() {
return await this._makeRequest<ISessionsAnswer>({
cmd: 'sessions.list',
});
}
private _getProxyFromURL(proxy: URL) {
return {
url: proxy.href,
username: proxy.username,
password: proxy.password,
};
}
/**
* Creates a session.
* @remarks All possible errors you can see in the {@link FlareSolverr._makeRequest | _makeRequest} method
* @returns Promise that is resolved to an object of type ISessionAnswer.
*/
async createSession(options?: Partial<ISessionCreateOptions>) {
return await this._makeRequest<ISessionAnswer>({
cmd: 'sessions.create',
session: options?.session,
proxy:
options?.proxy === undefined
? undefined
: this._getProxyFromURL(options.proxy),
});
}
/**
* Destroys a session.
* @remarks All possible errors you can see in the {@link FlareSolverr._makeRequest | _makeRequest} method
* @returns Promise that is resolved to an object of type IBaseAnswer.
*
*/
async destroySession(session: string) {
return await this._makeRequest<IBaseAnswer>({
cmd: 'sessions.destroy',
session: session,
});
}
/**
* This method works like a normal get request, but bypasses cloudflare's protection if cloudflare is present
* @remarks All possible errors you can see in the {@link FlareSolverr._makeRequest | _makeRequest} method
* @returns Promise that is resolved to an object of type IRequestAnswer.
*/
async get(options: IRequestGetOptions): Promise<IRequestAnswer> {
const response = await this._makeRequest<IRawRequestAnswer>({
cmd: 'request.get',
...options,
proxy:
options.proxy === undefined
? undefined
: this._getProxyFromURL(options.proxy),
});
return {
...response,
solution: {
...response.solution,
url: new URL(response.solution.url),
},
};
}
/**
* This method works like a normal post request, but bypasses cloudflare's protection if cloudflare is present
* @remarks All possible errors you can see in the {@link FlareSolverr._makeRequest | _makeRequest} method
* @returns Promise that is resolved to an object of type IRequestAnswer.
*/
async post(options: IRequestPostOptions): Promise<IRequestAnswer> {
const response = await this._makeRequest<IRawRequestAnswer>({
cmd: 'request.post',
...options,
proxy:
options.proxy === undefined
? undefined
: this._getProxyFromURL(options.proxy),
postData: Object.keys(options.postData)
.map(
(key) =>
encodeURIComponent(key) +
'=' +
encodeURIComponent(options.postData[key]),
)
.join('&'),
});
return {
...response,
solution: {
...response.solution,
url: new URL(response.solution.url),
},
};
}
}