F95API/src/scripts/classes/query/thread-search-query.ts

184 lines
4.8 KiB
TypeScript

// Copyright (c) 2021 MillenniumEarl
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
"use strict";
// Public modules from npm
import validator from "class-validator";
// Module from files
import { IQuery, TCategory, TQueryInterface } from "../../interfaces.js";
import { urls } from "../../constants/url.js";
import PrefixParser from "./../prefix-parser.js";
import { fetchPOSTResponse } from "../../network-helper.js";
import { AxiosResponse } from "axios";
import { GenericAxiosError } from "../errors.js";
import { Result } from "../result.js";
import Shared from "../../shared.js";
// Type definitions
export type TThreadOrder = "relevance" | "date" | "last_update" | "replies";
export default class ThreadSearchQuery implements IQuery {
//#region Private fields
static MIN_PAGE = 1;
//#endregion Private fields
//#region Properties
/**
* Keywords to use in the search.
*/
public keywords = "";
/**
* Indicates to search by checking only the thread titles and not the content.
*/
public onlyTitles = false;
/**
* The results must be more recent than the date indicated.
*/
public newerThan: Date = null;
/**
* The results must be older than the date indicated.
*/
public olderThan: Date = null;
public includedTags: string[] = [];
/**
* Tags to exclude from the search.
*/
public excludedTags: string[] = [];
/**
* Minimum number of answers that the thread must possess.
*/
public minimumReplies = 0;
public includedPrefixes: string[] = [];
public category: TCategory = null;
/**
* Results presentation order.
*/
public order: TThreadOrder = "relevance";
@validator.IsInt({
message: "$property expect an integer, received $value"
})
@validator.Min(ThreadSearchQuery.MIN_PAGE, {
message: "The minimum $property value must be $constraint1, received $value"
})
public page = 1;
itype: TQueryInterface = "ThreadSearchQuery";
//#endregion Properties
//#region Public methods
public validate(): boolean {
return validator.validateSync(this).length === 0;
}
public async execute(): Promise<Result<GenericAxiosError, AxiosResponse<any>>> {
// Check if the query is valid
if (!this.validate()) {
throw new Error(`Invalid query: ${validator.validateSync(this).join("\n")}`);
}
// Define the POST parameters
const params = this.preparePOSTParameters();
// Return the POST response
return fetchPOSTResponse(urls.SEARCH, params);
}
//#endregion Public methods
//#region Private methods
/**
* Prepare the parameters for post request with the data in the query.
*/
private preparePOSTParameters(): { [s: string]: string } {
// Local variables
const params = {};
// Ad the session token
params["_xfToken"] = Shared.session.token;
// Specify if only the title should be searched
if (this.onlyTitles) params["c[title_only]"] = "1";
// Add keywords
params["keywords"] = this.keywords ?? "*";
// Specify the scope of the search (only "threads/post")
params["search_type"] = "post";
// Set the dates
if (this.newerThan) {
const date = this.convertShortDate(this.newerThan);
params["c[newer_than]"] = date;
}
if (this.olderThan) {
const date = this.convertShortDate(this.olderThan);
params["c[older_than]"] = date;
}
// Set included and excluded tags (joined with a comma)
if (this.includedTags) params["c[tags]"] = this.includedTags.join(",");
if (this.excludedTags) params["c[excludeTags]"] = this.excludedTags.join(",");
// Set minimum reply number
if (this.minimumReplies > 0)
params["c[min_reply_count]"] = this.minimumReplies.toString();
// Add prefixes
const parser = new PrefixParser();
const ids = parser.prefixesToIDs(this.includedPrefixes);
for (let i = 0; i < ids.length; i++) {
const name = `c[prefixes][${i}]`;
params[name] = ids[i].toString();
}
// Set the category
params["c[child_nodes]"] = "1"; // Always set
if (this.category) {
const catID = this.categoryToID(this.category).toString();
params["c[nodes][0]"] = catID;
}
// Set the other values
params["order"] = this.order.toString();
params["page"] = this.page.toString();
return params;
}
/**
* Convert a date in the YYYY-MM-DD format taking into account the time zone.
*/
private convertShortDate(d: Date): string {
const offset = d.getTimezoneOffset();
d = new Date(d.getTime() - offset * 60 * 1000);
return d.toISOString().split("T")[0];
}
/**
* Gets the unique ID of the selected category.
*/
private categoryToID(category: TCategory): number {
const catMap = {
games: 2,
mods: 41,
comics: 40,
animations: 94,
assets: 95
};
return catMap[category as string];
}
//#endregion Private methods
}