UNPKG

scratch-l10n

Version:
327 lines (305 loc) • 10.8 kB
// interface to FreshDesk Solutions (knowledge base) api /** * Extend */ export interface HttpError extends Error { /** HTTP error code (if any) */ code: number | null | undefined /** Retry-After header value (if any) */ retryAfter: string | null | undefined } /** * Properties to provide when creating a Category throught the Freshdesk API. Also works for updates. * @see https://developers.freshdesk.com/api/#create_solution_category */ export interface FreshdeskCategoryCreate { /** Name of the solution category. Mandatory for Create. */ name: string /** Description of the solution category */ description?: string /** List of portal IDs where this category is visible */ visible_in_portals?: number[] } /** * Categories broadly classify your solutions page into several sections. * @see https://developers.freshdesk.com/api/#solution_category_attributes */ export interface FreshdeskCategory extends FreshdeskCategoryCreate { /** Unique ID of the solution category */ id?: number /** Solution Category creation timestamp */ created_at?: string /** Solution Category update timestamp */ updated_at?: string } /** * Properties to provide when creating a Folder throught the Freshdesk API. Also works for updates. * @see https://developers.freshdesk.com/api/#create_solution_folder */ export interface FreshdeskFolderCreate { /** Description of the solution folder */ description?: string /** Name of the solution folder */ name: string /** ID of the parent folder */ parent_folder_id?: number /** Accessibility of this folder. Please refer to Folder Properties table. */ visibility?: FreshdeskFolderVisibility /** IDs of the companies to whom this solution folder is visible */ company_ids?: number[] /** IDs of the contact segments to whom this solution folder is visible */ contact_segment_ids?: number[] /** IDs of the company segments to whom this solution folder is visible */ company_segment_ids?: number[] } /** * Related Solutions Articles and/or Folders are organized into Folders. Folders make it convenient for users to read * similar articles or navigate to other possible solutions to their problem. * @see https://developers.freshdesk.com/api/#solution_folder_attributes */ export interface FreshdeskFolder extends FreshdeskFolderCreate { /** Unique ID of the solution folder */ id?: number /** Parent category and folders in which the folder is placed */ hierarchy?: object[] /** Number of articles present inside a folder */ articles_count?: number /** Number of folders present inside a folder */ sub_folders_count?: number /** Solution Folder creation timestamp */ created_at?: string /** Solution Folder updated timestamp */ updated_at?: string } export enum FreshdeskFolderVisibility { AllUsers = 1, LoggedInUsers = 2, Agents = 3, SelectedCompanies = 4, Bots = 5, SelectedContactSegments = 6, SelectedCompanySegments = 7, } /** * Properties to provide when creating an Article throught the Freshdesk API. Also works for updates. * @see https://developers.freshdesk.com/api/#create_solution_article */ export interface FreshdeskArticleCreate { /** ID of the agent who created the solution article */ agent_id?: number /** Description of the solution article */ description: string /** Status of the solution article */ status: FreshdeskArticleStatus /** Meta data for search engine optimization. Allows meta_title, meta_description and meta_keywords */ seo_data?: FreshdeskSEOData /** Tags that have been associated with the solution article */ tags?: string[] /** Title of the solution article */ title: string } /** * Solution Articles or knowledge base posts promote self-help in your support portal. These should ideally cover all * aspects of your product or service like "how-to" instructions and FAQs. * @see https://developers.freshdesk.com/api/#solution_article_attributes */ export interface FreshdeskArticle extends FreshdeskArticleCreate { /** Unique ID of the solution article */ id?: number /** ID of the category to which the solution article belongs */ category_id?: number /** Description of the solution article in plain text */ description_text?: string /** ID of the folder to which the solution article belongs */ folder_id?: number /** Parent category and folders in which the article is placed */ hierarchy?: object[] /** Number of views for the solution article */ hits?: number /** Number of down votes for the solution article */ thumbs_down?: number /** Number of upvotes for the solution article */ thumbs_up?: number /** Solution Article creation timestamp */ created_at?: string /** Solution Article updated timestamp */ updated_at?: string } export enum FreshdeskArticleStatus { draft = 1, published = 2, } export interface FreshdeskSEOData { meta_title?: string meta_description?: string meta_keywords?: string[] } /** * Wrapper for Freshdesk's REST API */ export class FreshdeskApi { baseUrl: string private _auth: string defaultHeaders: { 'Content-Type': string; Authorization: string } rateLimited: boolean constructor(baseUrl: string, apiKey: string) { this.baseUrl = baseUrl this._auth = 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64') this.defaultHeaders = { 'Content-Type': 'application/json', Authorization: this._auth, } this.rateLimited = false } /** * Checks the status of a response. If status is not ok, or the body is not JSON, raise exception * @param res - The response object * @returns the response if it is ok */ checkStatus(res: Response) { if (res.ok) { if (res.headers.get('content-type')?.includes('application/json')) { return res } throw new Error(`response not json: ${res.headers.get('content-type')}`) } let message = `HTTP ${res.status} ${res.statusText} for ${res.url}` if (res.status === 403) { message += ` -- this item may have been deleted in Freshdesk, or the API key lacks permission.` } const err = new Error(message) as HttpError err.code = res.status if (res.status === 429) { err.retryAfter = res.headers.get('Retry-After') } throw err } async listCategories(): Promise<FreshdeskCategory[]> { const res = await fetch(`${this.baseUrl}/api/v2/solutions/categories`, { headers: this.defaultHeaders }) this.checkStatus(res) return (await res.json()) as FreshdeskCategory[] } async listFolders(category: FreshdeskCategory): Promise<FreshdeskFolder[]> { const res = await fetch(`${this.baseUrl}/api/v2/solutions/categories/${category.id}/folders`, { headers: this.defaultHeaders, }) this.checkStatus(res) return (await res.json()) as FreshdeskFolder[] } async listArticles(folder: FreshdeskFolder) { const res = await fetch(`${this.baseUrl}/api/v2/solutions/folders/${folder.id}/articles`, { headers: this.defaultHeaders, }) this.checkStatus(res) return (await res.json()) as FreshdeskArticle[] } async updateCategoryTranslation( id: FreshdeskCategory['id'], locale: string, body: FreshdeskCategoryCreate, ): Promise<FreshdeskCategory | -1> { if (this.rateLimited) { process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`) return -1 } try { const res = await fetch(`${this.baseUrl}/api/v2/solutions/categories/${id}/${locale}`, { method: 'put', body: JSON.stringify(body), headers: this.defaultHeaders, }) this.checkStatus(res) return (await res.json()) as FreshdeskCategory } catch (err) { const httpError = err as HttpError if (httpError.code === 404) { // not found, try create instead const res2 = await fetch(`${this.baseUrl}/api/v2/solutions/categories/${id}/${locale}`, { method: 'post', body: JSON.stringify(body), headers: this.defaultHeaders, }) this.checkStatus(res2) return (await res2.json()) as FreshdeskCategory } if (httpError.code === 429) { this.rateLimited = true } process.stdout.write(`Error processing id ${id} for locale ${locale}: ${httpError.message}\n`) throw err } } async updateFolderTranslation( id: FreshdeskFolder['id'], locale: string, body: FreshdeskFolderCreate, ): Promise<FreshdeskFolder | -1> { if (this.rateLimited) { process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`) return -1 } try { const res = await fetch(`${this.baseUrl}/api/v2/solutions/folders/${id}/${locale}`, { method: 'put', body: JSON.stringify(body), headers: this.defaultHeaders, }) this.checkStatus(res) return (await res.json()) as FreshdeskFolder } catch (err) { const httpError = err as HttpError if (httpError.code === 404) { // not found, try create instead const res2 = await fetch(`${this.baseUrl}/api/v2/solutions/folders/${id}/${locale}`, { method: 'post', body: JSON.stringify(body), headers: this.defaultHeaders, }) this.checkStatus(res2) return (await res2.json()) as FreshdeskFolder } if (httpError.code === 429) { this.rateLimited = true } process.stdout.write(`Error processing id ${id} for locale ${locale}: ${httpError.message}\n`) throw err } } async updateArticleTranslation( id: FreshdeskArticle['id'], locale: string, body: FreshdeskArticleCreate, ): Promise<FreshdeskArticle | -1> { if (this.rateLimited) { process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`) return -1 } try { const res = await fetch(`${this.baseUrl}/api/v2/solutions/articles/${id}/${locale}`, { method: 'put', body: JSON.stringify(body), headers: this.defaultHeaders, }) this.checkStatus(res) return (await res.json()) as FreshdeskArticle } catch (err) { const httpError = err as HttpError if (httpError.code === 404) { // not found, try create instead const res2 = await fetch(`${this.baseUrl}/api/v2/solutions/articles/${id}/${locale}`, { method: 'post', body: JSON.stringify(body), headers: this.defaultHeaders, }) this.checkStatus(res2) return (await res2.json()) as FreshdeskArticle } if (httpError.code === 429) { this.rateLimited = true } process.stdout.write(`Error processing id ${id} for locale ${locale}: ${httpError.message}\n`) throw err } } } export default FreshdeskApi