scratch-l10n
Version:
Localization for the Scratch 3.0 components
327 lines (305 loc) • 10.8 kB
text/typescript
// 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