UNPKG

r34-client

Version:

Client side library for interacting with a r34-json-api

280 lines (247 loc) 7.25 kB
import { AliasTag, AnyTag, api, ApiVersion, Artist } from "r34-types" import { getSupertags, init } from "./firebase" import firebase from "firebase" import { createSearchParams, ParamsRecord } from "./utils" import { serializeTagname, isSuggestionError, serializeAllTags, } from "./tagUtils" /** * Configure your client. */ export interface R34ClientOptions { /** * API version to use. */ version: ApiVersion /** * When set to true, the API provides additional, user-specific information like supertags. */ useFirebase: boolean /** * Configure how many posts are returned by default */ postLimit: number /** * Configure how many tags are returned by default */ tagLimit: number /** * How many times to retry failed requests */ requestRetries: number /** * Enables console logging */ verbose: boolean } /** * Can be a single backend or multiple. If there are multiple it will automatically switch when one goes down. * Valid backends include protocol and port and do not have a trailing slash. * @example "http://localhost:8080" * @example "https://my-api-server.herokuapp.com" */ export type BackendsUrls = string | string[] export class R34Client { //#region Properties private readonly backends: string[] private currentBackend: number readonly useFirebase: boolean postLimit: number tagLimit: number version: ApiVersion requestRetries: number verbose: boolean //#endregion //#region Management and Utils /** * Creates a new R34Client. */ constructor(backends: BackendsUrls, options: Partial<R34ClientOptions> = {}) { this.backends = typeof backends === "string" ? [backends] : [...backends] this.version = options.version || "v2" this.useFirebase = options.useFirebase || false this.postLimit = options.postLimit || 20 this.tagLimit = options.tagLimit || 20 this.requestRetries = options.requestRetries || 1 this.verbose = options.verbose || false this.currentBackend = 0 if (this.useFirebase) { init() } } /** * Switches to the next backend in the list. When it runs out of backends returns to the first one. */ private useNextBackend() { this.currentBackend = (this.currentBackend + 1) % this.backends.length } /** * Combines path with current backend and version to create a valid url */ private getFullUrl(path: string, params: ParamsRecord): URL { const host = this.backends[this.currentBackend] const version = this.version const search = createSearchParams(params).toString() return new URL(`${host}/${version}/${path}?${search}`) } /** * Wraps fetch with failover functionality */ private async fetchWithFailover( path: string, params: ParamsRecord = {}, fetchOptions: Partial<RequestInit> = {}, retries = 0 ): Promise<Response> { const url = this.getFullUrl(path, { ...params }) return fetch(url.toString(), fetchOptions).then((res) => { if ( !res.ok && this.backends.length > 1 && retries < this.requestRetries ) { if (this.verbose) { console.warn( `Fetch failed against ${url}. Status: ${res.status}. Retrying against next backend.\nDetails:`, res ) } this.useNextBackend() return this.fetchWithFailover(path, params, fetchOptions, retries + 1) } return res }) } //#endregion //#region API //#region Tags /** * Retrieves tags given a searchTerm */ private async fetchTags( params: api.params.Tags ): Promise<api.responses.Tags> { const paramsInternal = { limit: this.tagLimit, ...params } if (params.name) { paramsInternal.name = serializeTagname(params.name) } const res = await this.fetchWithFailover("tags", paramsInternal) return res.json() } async getTags(params: api.params.Tags) { try { const result = await this.fetchTags(params) if (isSuggestionError(result)) { return result } let tags = result as AnyTag[] const name = params.name if (params.supertags && name) { try { const supertags = await getSupertags() if (supertags) { const matchingSupertags = Object.entries(supertags) .filter(([name, details]) => name.toLowerCase().includes(name.toLowerCase()) ) .map(([name, details]) => ({ name, ...details, })) tags = [...matchingSupertags, ...tags] } } catch { // do nothing as supertags are only for registered users. } } return tags } catch (err) { console.warn("Failed to get tags:", err) return [] } } //#endregion //#region Posts /** * This function can be used to retrieve a number of posts from the backend. */ async getPosts(params: api.params.Posts) { try { let paramsInternal: Omit<api.params.Posts, "tags"> & { tags?: string } = { limit: this.postLimit, ...(params as Omit<api.params.Posts, "tags">), } if (params.tags) { paramsInternal.tags = serializeAllTags(params.tags) } const apiResponse = await this.fetchWithFailover( "posts", paramsInternal, { headers: { Authorization: `Bearer ${await firebase .auth() .currentUser?.getIdToken()}`, }, } ) const data: api.responses.Posts = await apiResponse.json() return data } catch (err) { console.warn("Failed to get posts:", err) return { count: 0, posts: [] } } } //#endregion //#region Aliases /** * Returns all aliases for a given tag */ async getAliases(params: api.params.Aliases) { try { const aliases: AliasTag[] = await ( await this.fetchWithFailover("aliases", params) ).json() return aliases } catch (err) { console.warn("Failed to get aliases:", err) return [] } } //#endregion //#region Comments /** * Returns all comments for a given post. */ async getComments( params: api.params.Comments ): Promise<api.responses.Comments> { try { // This does not use my api but it is included here for simplicity. // Therefore, no failover etc. return (await fetch(params.commentsUrl)).json() } catch (err) { console.warn("Failed to get comments:", err) return [] } } //#endregion //#region Artists async getArtist(params: api.params.Artist) { try { const artist: Artist = await ( await this.fetchWithFailover("artist", params) ).json() return artist } catch (err) { console.warn("Failed to get aliases:", err) return undefined } } //#endregion //#endregion }