From 9116a4f3ef4a96950714f50e4a196166c1d16193 Mon Sep 17 00:00:00 2001 From: Ali Awari Date: Wed, 2 Oct 2024 16:54:31 -0400 Subject: [PATCH] feat: added prettier, and getMediaContainerStatus --- .eslintrc.js | 75 ++- package-lock.json | 218 +++++++ package.json | 4 + src/index.ts | 1513 ++++++++++++++++++++++++--------------------- 4 files changed, 1067 insertions(+), 743 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f6e3215..beae545 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,40 +3,49 @@ module.exports = { env: { node: true, }, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + ], // add your custom rules here rules: { - semi: ['error', 'always'], - 'no-extra-semi': 'error', - 'no-extra-parens': 'off', - 'comma-dangle': ['error', 'always-multiline'], - 'space-before-function-paren': ['error', { - anonymous: 'always', - named: 'never', - asyncArrow: 'always', - }], - 'func-call-spacing': ['error', 'never'], - 'no-console': 'off', - 'no-unused-expression': 'off', - 'no-useless-constructor': 'off', - 'arrow-parens': 'off', - 'no-use-before-define': 'off', - 'no-return-assign': 'off', - 'quotes': ['error', 'single'], - 'member-access': 'off', - 'member-ordering': 'off', - 'object-literal-sort-keys': 'off', - 'no-trailing-spaces': 'error', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-extra-parens': ['off'], - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/no-unused-vars': ['error', { - varsIgnorePattern: '^_', - }], + semi: ["error", "always"], + "no-extra-semi": "error", + "no-extra-parens": "off", + "comma-dangle": ["error", "always-multiline"], + "space-before-function-paren": [ + "error", + { + anonymous: "always", + named: "never", + asyncArrow: "always", + }, + ], + "func-call-spacing": ["error", "never"], + "no-console": "off", + "no-unused-expression": "off", + "no-useless-constructor": "off", + "arrow-parens": "off", + "no-use-before-define": "off", + "no-return-assign": "off", + "member-access": "off", + "member-ordering": "off", + "object-literal-sort-keys": "off", + "no-trailing-spaces": "error", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-extra-parens": ["off"], + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + varsIgnorePattern: "^_", + }, + ], }, }; diff --git a/package-lock.json b/package-lock.json index 87b990e..1444214 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,10 @@ "chai": "^4.3.4", "dotenv": "^16.0.3", "eslint": "^8.30.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", "mocha": "^10.0.0", + "sinon": "^19.0.2", "ts-node": "^10.9.1", "typedoc": "^0.23.23", "typescript": "^4.2.4" @@ -189,6 +192,62 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz", + "integrity": "sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -983,6 +1042,48 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -1116,6 +1217,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -1590,6 +1697,12 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -1627,6 +1740,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1824,6 +1943,19 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1928,6 +2060,15 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -1967,6 +2108,34 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2184,6 +2353,33 @@ "vscode-textmate": "^8.0.0" } }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2243,6 +2439,28 @@ "node": ">=8" } }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index e520dff..7106579 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "build:esm": "tsc -b tsconfig.esm.json", "build-doc": "typedoc src/index.ts --out tsdocs", "lint": "eslint --ext \".ts\" --ignore-path .gitignore .", + "test": "npm run mocha 'test/**/*.test.ts'", "prepublish": "npm run build" }, "repository": "github:solojungle/threads-ts", @@ -42,7 +43,10 @@ "chai": "^4.3.4", "dotenv": "^16.0.3", "eslint": "^8.30.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", "mocha": "^10.0.0", + "sinon": "^19.0.2", "ts-node": "^10.9.1", "typedoc": "^0.23.23", "typescript": "^4.2.4" diff --git a/src/index.ts b/src/index.ts index a1261a1..3190aeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,739 +1,832 @@ -import axios, { AxiosResponse } from 'axios'; +import axios, { AxiosResponse } from "axios"; /** * Fields when trying to get profile information about a Threads user. */ type ProfileFields = - | 'id' - | 'username' - | 'name' - | 'threads_profile_picture_url' - | 'threads_biography'; + | "id" + | "username" + | "name" + | "threads_profile_picture_url" + | "threads_biography"; type RetrieveRepliesFields = - | 'id' - | 'text' - | 'username' - | 'permalink' - | 'timestamp' - | 'media_product_type' - | 'media_type' - | 'media_url' - | 'shortcode' - | 'thumbnail_url' - | 'children' - | 'is_quote_post' - | 'has_replies' - | 'root_post' - | 'replied_to' - | 'is_reply' - | 'is_reply_owned_by_me' - | 'hide_status' - | 'reply_audience'; + | "id" + | "text" + | "username" + | "permalink" + | "timestamp" + | "media_product_type" + | "media_type" + | "media_url" + | "shortcode" + | "thumbnail_url" + | "children" + | "is_quote_post" + | "has_replies" + | "root_post" + | "replied_to" + | "is_reply" + | "is_reply_owned_by_me" + | "hide_status" + | "reply_audience"; type Scope = - | 'threads_basic' - | 'threads_content_publish' - | 'threads_manage_insights' - | 'threads_manage_replies' - | 'threads_read_replies'; + | "threads_basic" + | "threads_content_publish" + | "threads_manage_insights" + | "threads_manage_replies" + | "threads_read_replies"; + +interface ContainerStatus { + status: "EXPIRED" | "ERROR" | "FINISHED" | "IN_PROGRESS" | "PUBLISHED"; + id: string; + error_message?: string; +} + +type TimePeriod = "day" | "week" | "days_28" | "lifetime"; export interface ThreadsAPIConfig { - clientId: string; - clientSecret: string; - redirectUri: string; - /** - * The scopes of access granted by the access_token expressed as a list of comma-delimited, or space-delimited, case-sensitive strings. - */ - scope: Scope[]; + clientId: string; + clientSecret: string; + redirectUri: string; + /** + * The scopes of access granted by the access_token expressed as a list of comma-delimited, or space-delimited, case-sensitive strings. + */ + scope: Scope[]; } /** * Note: Type CAROUSEL is not available for single thread posts. */ -type MediaType = 'TEXT' | 'IMAGE' | 'VIDEO' | 'CAROUSEL'; -type ReplyControl = 'everyone' | 'accounts_you_follow' | 'mentioned_only'; +export type MediaType = "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL"; +type ReplyControl = "everyone" | "accounts_you_follow" | "mentioned_only"; interface MediaContainer { - id: string; + id: string; +} + +// Available metric names for both Media and User Insights +type MetricName = + | "views" + | "likes" + | "replies" + | "reposts" + | "quotes" + | "followers_count" + | "follower_demographics"; + +// Structure for a single metric value (used in Media Insights) +interface MetricValue { + value: number; +} + +// Structure for a single time series value (used in User Insights) +interface TimeSeriesValue { + value: number; + end_time: string; +} + +// Structure for a total value (used in User Insights) +interface TotalValue { + value: number; +} + +// Structure for a single metric in Media Insights +interface MediaMetric { + name: MetricName; + period: TimePeriod; + values: MetricValue[]; + title: string; + description: string; + id: string; +} + +// Structure for a single metric in User Insights +interface UserMetric { + name: MetricName; + period: TimePeriod; + values?: TimeSeriesValue[]; + total_value?: TotalValue; + title: string; + description: string; + id: string; +} + +// Main response structure for Media Insights +interface ThreadsMediaInsightsResponse { + data: MediaMetric[]; } -interface ThreadInsights { - [key: string]: number; +// Main response structure for User Insights +interface ThreadsUserInsightsResponse { + data: UserMetric[]; } -interface UserInsights { - [key: string]: number; +// Parameters for the User Insights API request +interface ThreadsUserInsightsParams { + metric: MetricName | MetricName[]; + options: { + since?: number; // Unix timestamp + until?: number; // Unix timestamp + }; } interface TokenResponse { - /** - * A token that can be sent to a Threads API. - */ - access_token: string; - /** - * Identifies the type of token returned. At this time, this field always has the value Bearer. - */ - token_type: string; - /** - * The time in seconds at which this token is thought to expire. - */ - expires_in: number; + /** + * A token that can be sent to a Threads API. + */ + access_token: string; + /** + * Identifies the type of token returned. At this time, this field always has the value Bearer. + */ + token_type: string; + /** + * The time in seconds at which this token is thought to expire. + */ + expires_in: number; } export class ThreadsAPI { - private config: ThreadsAPIConfig; - - private accessToken: string | null = null; - - private baseUrl = 'https://graph.threads.net/v1.0/'; - - constructor(config: ThreadsAPIConfig) { - this.config = config; - } - - /** - * Set the access token for the API - * @param accessToken The access token - * @returns void - */ - setAccessToken(accessToken: string): void { - this.accessToken = accessToken; - } - - // Token Management - async refreshTokenIfNeeded({ - accessToken, - expiresIn, - }: { - accessToken: string; - expiresIn: number; - }): Promise { - const currentTime = Math.floor(Date.now() / 1000); - // If there is less than a week left on the token, refresh it - if (currentTime >= expiresIn - 604800) { - const response = await this.refreshLongLivedToken(accessToken); - const { access_token: refreshedAccessToken } = response; - this.accessToken = refreshedAccessToken; - return response; - } - - return { - access_token: accessToken, - token_type: 'Bearer', - expires_in: expiresIn, - }; - } - - /** - * Generate the authorization URL for OAuth flow - * @param state Optional state parameter for OAuth - * @returns The authorization URL - */ - getAuthorizationUrl(state?: string): string { - const baseUrl = 'https://threads.net/oauth/authorize'; - const params = new URLSearchParams({ - client_id: this.config.clientId, - redirect_uri: this.config.redirectUri, - scope: this.config.scope.join(','), - response_type: 'code', - ...(state && { state }), - }); - - return `${baseUrl}?${params.toString()}`; - } - - /** - * Exchange authorization code for a short-lived access token - * @param code The authorization code - * @returns Object containing short-lived access token and user ID - */ - async getAccessToken(code: string): Promise { - const url = `${this.baseUrl}oauth/access_token`; - const params = new URLSearchParams({ - client_id: this.config.clientId, - client_secret: this.config.clientSecret, - grant_type: 'authorization_code', - redirect_uri: this.config.redirectUri, - code, - }); - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - this.accessToken = response.access_token; - return response; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Exchange short-lived token for long-lived token - * @param shortLivedToken The short-lived access token - * @returns Object containing long-lived access token - */ - async getLongLivedToken(shortLivedToken: string): Promise { - const url = `${this.baseUrl}access_token`; - const params = new URLSearchParams({ - grant_type: 'th_exchange_token', - client_secret: this.config.clientSecret, - access_token: shortLivedToken, - }); - - try { - const response = await this.makeRequest({ - url, - method: 'GET', - params, - }); - return { ...response }; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Refresh long-lived token - * @param longLivedToken The long-lived access token to refresh - * @returns The new long-lived access token - */ - async refreshLongLivedToken(longLivedToken: string): Promise { - const url = `${this.baseUrl}refresh_access_token`; - const params = new URLSearchParams({ - grant_type: 'th_refresh_token', - access_token: longLivedToken, - }); - - try { - const response = await this.makeRequest({ - url, - method: 'GET', - params, - }); - return { ...response }; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Create a media container for a thread post - * @param userId The user ID - * @param mediaType The type of media - * @param mediaUrl Optional URL for image or video - * @param text Optional text content - * @returns The creation ID of the media container - */ - async createMediaContainer({ - userId, - mediaType, - mediaUrl, - text, - }: { - userId: string; - mediaType: MediaType; - mediaUrl?: string; - text?: string; - }): Promise { - const url = `${this.baseUrl}${userId}/threads`; - const params: Record = { - media_type: mediaType, - ...(mediaType === 'IMAGE' && mediaUrl && { image_url: mediaUrl }), - ...(mediaType === 'VIDEO' && mediaUrl && { video_url: mediaUrl }), - ...(mediaType === 'TEXT' && text && { text }), - }; - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - return response.id; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Publish a media container - * @param userId The user ID - * @param creationId The creation ID of the media container - * @returns The ID of the published thread - */ - async publishMediaContainer({ - userId, - creationId, - }: { - userId: string; - creationId: string; - }): Promise { - const url = `${this.baseUrl}${userId}/threads_publish`; - const params = new URLSearchParams({ - creation_id: creationId, - }); - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - return response.id; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Create a carousel item container - * @param userId The user ID - * @param mediaType The type of media (IMAGE or VIDEO) - * @param mediaUrl The URL of the media - * @returns The creation ID of the carousel item container - */ - async createCarouselItemContainer({ - userId, - mediaType, - mediaUrl, - }: { - userId: string; - mediaType: 'IMAGE' | 'VIDEO'; - mediaUrl: string; - }): Promise { - const url = `${this.baseUrl}${userId}/threads`; - const params: Record = { - media_type: mediaType, - is_carousel_item: 'true', - ...(mediaType === 'IMAGE' - ? { image_url: mediaUrl } - : { video_url: mediaUrl }), - }; - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - return response.id; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Create a carousel container - * @param userId The user ID - * @param children Array of creation IDs for carousel items - * @param text Optional text content - * @returns The creation ID of the carousel container - */ - async createCarouselContainer({ - userId, - children, - text, - }: { - userId: string; - children: string[]; - text?: string; - }): Promise { - const url = `${this.baseUrl}${userId}/threads`; - const params: Record = { - media_type: 'CAROUSEL', - children: children.join(','), - ...(text && { text }), - }; - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - return response.id; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve user's threads - * @param userId The user ID - * @param fields Array of fields to retrieve - * @param options Optional parameters for pagination and date range - * @returns Array of user's threads - */ - async getUserThreads({ - userId, - fields, - options, - }: { - userId: string; - fields: string[]; - options?: { since?: string; until?: string; limit?: number }; - }): Promise { - const url = `${this.baseUrl}${userId}/threads`; - const params: Record = { - fields: fields.join(','), - ...(options?.limit && { limit: options.limit.toString() }), - ...(options?.since && { since: options.since }), - ...(options?.until && { until: options.until }), - }; - - try { - const response = await this.makeRequest<{ data: any[] }>({ - url, - method: 'GET', - params, - }); - return response.data; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve a single threads media object - * @param mediaId The ID of the media object - * @param fields Array of fields to retrieve - * @returns The threads media object - */ - async getThreadsMediaObject({ - mediaId, - fields, - }: { - mediaId: string; - fields: string[]; - }): Promise { - const url = `${this.baseUrl}${mediaId}`; - const params = { - fields: fields.join(','), - }; - - try { - return await this.makeRequest({ url, method: 'GET', params }); - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve a user's profile - * @param userId The user ID - * @param fields Array of fields to retrieve - * @returns The user's profile - */ - async getUserProfile({ - userId, - fields, - }: { - userId: string; - fields: ProfileFields[]; - }): Promise { - const url = `${this.baseUrl}${userId}`; - const params = { - fields: fields.join(','), - }; - - try { - return await this.makeRequest({ url, method: 'GET', params }); - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve replies to a thread - * @param mediaId The ID of the thread - * @param fields Array of fields to retrieve - * @param reverse Whether to reverse the order of replies - * @returns Array of replies - */ - async getReplies({ - mediaId, - fields, - reverse = true, - }: { - mediaId: string; - fields: RetrieveRepliesFields[]; - reverse?: boolean; - }): Promise { - const url = `${this.baseUrl}${mediaId}/replies`; - const params = { - fields: fields.join(','), - reverse: reverse.toString(), - }; - - try { - const response = await this.makeRequest<{ data: any[] }>({ - url, - method: 'GET', - params, - }); - return response.data; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve a conversation thread - * @param mediaId The ID of the thread - * @param fields Array of fields to retrieve - * @param reverse Whether to reverse the order of conversation - * @returns Array of conversation items - */ - async getConversation({ - mediaId, - fields, - reverse = true, - }: { - mediaId: string; - fields: string[]; - reverse?: boolean; - }): Promise { - const url = `${this.baseUrl}${mediaId}/conversation`; - const params = { - fields: fields.join(','), - reverse: reverse.toString(), - }; - - try { - const response = await this.makeRequest<{ data: any[] }>({ - url, - method: 'GET', - params, - }); - return response.data; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Hide or unhide a reply - * @param replyId The ID of the reply - * @param hide Whether to hide (true) or unhide (false) the reply - * @returns Whether the operation was successful - */ - async hideReply({ - replyId, - hide, - }: { - replyId: string; - hide: boolean; - }): Promise { - const url = `${this.baseUrl}${replyId}/manage_reply`; - const params = new URLSearchParams({ - hide: hide.toString(), - }); - - try { - const response = await this.makeRequest<{ success: boolean }>({ - url, - method: 'POST', - params, - }); - return response.success; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Respond to a reply - * @param userId The user ID - * @param mediaType The type of media for the response - * @param text The text content of the response - * @param replyToId The ID of the thread to reply to - * @returns The ID of the created reply - */ - async respondToReply({ - userId, - mediaType, - text, - replyToId, - }: { - userId: string; - mediaType: MediaType; - text: string; - replyToId: string; - }): Promise { - const url = `${this.baseUrl}${userId}/threads`; - const params = { - media_type: mediaType, - text, - reply_to_id: replyToId, - }; - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - return response.id; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Control who can reply to a thread - * @param userId The user ID - * @param mediaType The type of media for the thread - * @param text The text content of the thread - * @param replyControl The reply control setting - * @returns The ID of the created thread - */ - async controlWhoCanReply({ - userId, - mediaType, - text, - replyControl, - }: { - userId: string; - mediaType: MediaType; - text: string; - replyControl: ReplyControl; - }): Promise { - const url = `${this.baseUrl}${userId}/threads`; - const params = { - media_type: mediaType, - text, - reply_control: replyControl, - }; - - try { - const response = await this.makeRequest({ - url, - method: 'POST', - params, - }); - return response.id; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve media insights - * @param mediaId The ID of the media - * @param metrics Array of metrics to retrieve - * @returns The media insights - */ - async getMediaInsights({ - mediaId, - metrics, - }: { - mediaId: string; - metrics: string[]; - }): Promise { - const url = `${this.baseUrl}${mediaId}/insights`; - const params = { - metric: metrics.join(','), - }; - - try { - const response = await this.makeRequest<{ data: ThreadInsights }>({ - url, - method: 'GET', - params, - }); - return response.data; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Retrieve user insights - * @param userId The user ID - * @param metrics Array of metrics to retrieve - * @param options Optional parameters for date range - * @returns The user insights - */ - async getUserInsights({ - userId, - metrics, - options, - }: { - userId: string; - metrics: string[]; - options?: { since?: number; until?: number }; - }): Promise { - const url = `${this.baseUrl}${userId}/threads_insights`; - const params: Record = { - metric: metrics.join(','), - ...(options?.since && { since: options.since.toString() }), - ...(options?.until && { until: options.until.toString() }), - }; - - try { - const response = await this.makeRequest<{ data: UserInsights }>({ - url, - method: 'GET', - params, - }); - return response.data; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Make a request to the Threads API - * @param url The API endpoint URL - * @param method The HTTP method - * @param params The request parameters - * @returns The response data - */ - private async makeRequest({ - url, - method, - params, - }: { - url: string; - method: 'GET' | 'POST'; - params: Record | URLSearchParams; - }): Promise { - const config = { - method, - url, - ...(method === 'GET' ? { params } : { data: params }), - headers: { - ...(this.accessToken && { - Authorization: `Bearer ${this.accessToken}`, - }), - }, - }; - - try { - const response: AxiosResponse = await axios(config); - return response.data; - } catch (error) { - throw this.handleError(error); - } - } - - /** - * Handle errors from API requests - * @param error The error object - * @returns The error message - */ - // eslint-disable-next-line class-methods-use-this - private handleError(error: any): Error { - if (axios.isAxiosError(error)) { - if (error.response) { - throw new Error(error.response.data.error_message || error.message); - } - throw new Error(error.message); - } - throw error; - } + private config: ThreadsAPIConfig; + + private accessToken: string | null = null; + + private baseUrl = "https://graph.threads.net/v1.0/"; + + constructor(config: ThreadsAPIConfig) { + this.config = config; + } + + /** + * Set the access token for the API + * @param accessToken The access token + * @returns void + */ + setAccessToken(accessToken: string): void { + this.accessToken = accessToken; + } + + // Token Management + async refreshTokenIfNeeded({ + accessToken, + expiresIn, + }: { + accessToken: string; + expiresIn: number; + }): Promise { + const currentTime = Math.floor(Date.now() / 1000); + // If there is less than a week left on the token, refresh it + if (currentTime >= expiresIn - 604800) { + const response = await this.refreshLongLivedToken(accessToken); + const { access_token: refreshedAccessToken } = response; + this.accessToken = refreshedAccessToken; + return response; + } + + return { + access_token: accessToken, + token_type: "Bearer", + expires_in: expiresIn, + }; + } + + /** + * Generate the authorization URL for OAuth flow + * @param state Optional state parameter for OAuth + * @returns The authorization URL + */ + getAuthorizationUrl(state?: string): string { + const baseUrl = "https://threads.net/oauth/authorize"; + const params = new URLSearchParams({ + client_id: this.config.clientId, + redirect_uri: this.config.redirectUri, + scope: this.config.scope.join(","), + response_type: "code", + ...(state && { state }), + }); + + return `${baseUrl}?${params.toString()}`; + } + + /** + * Exchange authorization code for a short-lived access token + * @param code The authorization code + * @returns Object containing short-lived access token and user ID + */ + async getAccessToken(code: string): Promise { + const url = `${this.baseUrl}oauth/access_token`; + const params = new URLSearchParams({ + client_id: this.config.clientId, + client_secret: this.config.clientSecret, + grant_type: "authorization_code", + redirect_uri: this.config.redirectUri, + code, + }); + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + this.accessToken = response.access_token; + return response; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Exchange short-lived token for long-lived token + * @param shortLivedToken The short-lived access token + * @returns Object containing long-lived access token + */ + async getLongLivedToken(shortLivedToken: string): Promise { + const url = `${this.baseUrl}access_token`; + const params = new URLSearchParams({ + grant_type: "th_exchange_token", + client_secret: this.config.clientSecret, + access_token: shortLivedToken, + }); + + try { + const response = await this.makeRequest({ + url, + method: "GET", + params, + }); + return { ...response }; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Refresh long-lived token + * @param longLivedToken The long-lived access token to refresh + * @returns The new long-lived access token + */ + async refreshLongLivedToken(longLivedToken: string): Promise { + const url = `${this.baseUrl}refresh_access_token`; + const params = new URLSearchParams({ + grant_type: "th_refresh_token", + access_token: longLivedToken, + }); + + try { + const response = await this.makeRequest({ + url, + method: "GET", + params, + }); + return { ...response }; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Create a media container for a thread post + * @param userId The user ID + * @param mediaType The type of media + * @param mediaUrl Optional URL for image or video + * @param text Optional text content + * @returns The creation ID of the media container + */ + async createMediaContainer({ + userId, + mediaType, + mediaUrl, + text, + }: { + userId: string; + mediaType: MediaType; + mediaUrl?: string; + text?: string; + }): Promise { + const url = `${this.baseUrl}${userId}/threads`; + const params: Record = { + media_type: mediaType, + ...(mediaType === "IMAGE" && mediaUrl && { image_url: mediaUrl }), + ...(mediaType === "VIDEO" && mediaUrl && { video_url: mediaUrl }), + ...(mediaType === "TEXT" && text && { text }), + }; + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + return response.id; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Publish a media container + * @param userId The user ID + * @param creationId The creation ID of the media container + * @returns The ID of the published thread + */ + async publishMediaContainer({ + userId, + creationId, + }: { + userId: string; + creationId: string; + }): Promise { + const url = `${this.baseUrl}${userId}/threads_publish`; + const params = new URLSearchParams({ + creation_id: creationId, + }); + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + return response.id; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Checks the status of a media container + * @param containerId The ID of the media container + * @returns The status of the media container + * @note Recommended querying a container's status once per minute, for no more than 5 minutes. + */ + async getMediaContainerStatus(containerId: string): Promise { + const url = `${this.baseUrl}${containerId}`; + const params = new URLSearchParams({ + fields: "status,error_message", + }); + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + + return response; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Create a carousel item container + * @param userId The user ID + * @param mediaType The type of media (IMAGE or VIDEO) + * @param mediaUrl The URL of the media + * @returns The creation ID of the carousel item container + */ + async createCarouselItemContainer({ + userId, + mediaType, + mediaUrl, + }: { + userId: string; + mediaType: "IMAGE" | "VIDEO"; + mediaUrl: string; + }): Promise { + const url = `${this.baseUrl}${userId}/threads`; + const params: Record = { + media_type: mediaType, + is_carousel_item: "true", + ...(mediaType === "IMAGE" + ? { image_url: mediaUrl } + : { video_url: mediaUrl }), + }; + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + return response.id; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Create a carousel container + * @param userId The user ID + * @param children Array of creation IDs for carousel items + * @param text Optional text content + * @returns The creation ID of the carousel container + */ + async createCarouselContainer({ + userId, + children, + text, + }: { + userId: string; + children: string[]; + text?: string; + }): Promise { + const url = `${this.baseUrl}${userId}/threads`; + const params: Record = { + media_type: "CAROUSEL", + children: children.join(","), + ...(text && { text }), + }; + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + return response.id; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve user's threads + * @param userId The user ID + * @param fields Array of fields to retrieve + * @param options Optional parameters for pagination and date range + * @returns Array of user's threads + */ + async getUserThreads({ + userId, + fields, + options, + }: { + userId: string; + fields: string[]; + options?: { since?: string; until?: string; limit?: number }; + }): Promise { + const url = `${this.baseUrl}${userId}/threads`; + const params: Record = { + fields: fields.join(","), + ...(options?.limit && { limit: options.limit.toString() }), + ...(options?.since && { since: options.since }), + ...(options?.until && { until: options.until }), + }; + + try { + const response = await this.makeRequest<{ data: any[] }>({ + url, + method: "GET", + params, + }); + return response.data; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve a single threads media object + * @param mediaId The ID of the media object + * @param fields Array of fields to retrieve + * @returns The threads media object + */ + async getThreadsMediaObject({ + mediaId, + fields, + }: { + mediaId: string; + fields: string[]; + }): Promise { + const url = `${this.baseUrl}${mediaId}`; + const params = { + fields: fields.join(","), + }; + + try { + return await this.makeRequest({ url, method: "GET", params }); + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve a user's profile + * @param userId The user ID + * @param fields Array of fields to retrieve + * @returns The user's profile + */ + async getUserProfile({ + userId, + fields, + }: { + userId: string; + fields: ProfileFields[]; + }): Promise { + const url = `${this.baseUrl}${userId}`; + const params = { + fields: fields.join(","), + }; + + try { + return await this.makeRequest({ url, method: "GET", params }); + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve replies to a thread + * @param mediaId The ID of the thread + * @param fields Array of fields to retrieve + * @param reverse Whether to reverse the order of replies + * @returns Array of replies + */ + async getReplies({ + mediaId, + fields, + reverse = true, + }: { + mediaId: string; + fields: RetrieveRepliesFields[]; + reverse?: boolean; + }): Promise { + const url = `${this.baseUrl}${mediaId}/replies`; + const params = { + fields: fields.join(","), + reverse: reverse.toString(), + }; + + try { + const response = await this.makeRequest<{ data: any[] }>({ + url, + method: "GET", + params, + }); + return response.data; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve a conversation thread + * @param mediaId The ID of the thread + * @param fields Array of fields to retrieve + * @param reverse Whether to reverse the order of conversation + * @returns Array of conversation items + */ + async getConversation({ + mediaId, + fields, + reverse = true, + }: { + mediaId: string; + fields: string[]; + reverse?: boolean; + }): Promise { + const url = `${this.baseUrl}${mediaId}/conversation`; + const params = { + fields: fields.join(","), + reverse: reverse.toString(), + }; + + try { + const response = await this.makeRequest<{ data: any[] }>({ + url, + method: "GET", + params, + }); + return response.data; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Hide or unhide a reply + * @param replyId The ID of the reply + * @param hide Whether to hide (true) or unhide (false) the reply + * @returns Whether the operation was successful + */ + async hideReply({ + replyId, + hide, + }: { + replyId: string; + hide: boolean; + }): Promise { + const url = `${this.baseUrl}${replyId}/manage_reply`; + const params = new URLSearchParams({ + hide: hide.toString(), + }); + + try { + const response = await this.makeRequest<{ success: boolean }>({ + url, + method: "POST", + params, + }); + return response.success; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Respond to a reply + * @param userId The user ID + * @param mediaType The type of media for the response + * @param text The text content of the response + * @param replyToId The ID of the thread to reply to + * @returns The ID of the created reply + */ + async respondToReply({ + userId, + mediaType, + text, + replyToId, + }: { + userId: string; + mediaType: MediaType; + text: string; + replyToId: string; + }): Promise { + const url = `${this.baseUrl}${userId}/threads`; + const params = { + media_type: mediaType, + text, + reply_to_id: replyToId, + }; + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + return response.id; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Control who can reply to a thread + * @param userId The user ID + * @param mediaType The type of media for the thread + * @param text The text content of the thread + * @param replyControl The reply control setting + * @returns The ID of the created thread + */ + async controlWhoCanReply({ + userId, + mediaType, + text, + replyControl, + }: { + userId: string; + mediaType: MediaType; + text: string; + replyControl: ReplyControl; + }): Promise { + const url = `${this.baseUrl}${userId}/threads`; + const params = { + media_type: mediaType, + text, + reply_control: replyControl, + }; + + try { + const response = await this.makeRequest({ + url, + method: "POST", + params, + }); + return response.id; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve media insights + * @param mediaId The ID of the media + * @param metrics Array of metrics to retrieve + * @returns The media insights + */ + async getMediaInsights({ + mediaId, + metrics, + }: { + mediaId: string; + metrics: string[]; + }): Promise { + const url = `${this.baseUrl}${mediaId}/insights`; + const params = { + metric: metrics.join(","), + }; + + try { + const response = await this.makeRequest<{ + data: ThreadsMediaInsightsResponse; + }>({ + url, + method: "GET", + params, + }); + return response.data; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Retrieve user insights + * @param userId The user ID + * @param metrics Array of metrics to retrieve + * @param options Optional parameters for date range + * @returns The user insights + */ + async getUserInsights({ + userId, + metric, + options, + }: { + userId: string; + } & ThreadsUserInsightsParams): Promise { + const url = `${this.baseUrl}${userId}/threads_insights`; + const params: Record = { + metric: Array.isArray(metric) ? metric.join(",") : metric, + ...(options?.since && { since: options.since.toString() }), + ...(options?.until && { until: options.until.toString() }), + }; + + try { + const response = await this.makeRequest<{ + data: ThreadsUserInsightsResponse; + }>({ + url, + method: "GET", + params, + }); + return response.data; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Make a request to the Threads API + * @param url The API endpoint URL + * @param method The HTTP method + * @param params The request parameters + * @returns The response data + */ + private async makeRequest({ + url, + method, + params, + }: { + url: string; + method: "GET" | "POST"; + params: Record | URLSearchParams; + }): Promise { + const config = { + method, + url, + ...(method === "GET" ? { params } : { data: params }), + headers: { + ...(this.accessToken && { + Authorization: `Bearer ${this.accessToken}`, + }), + }, + }; + + try { + const response: AxiosResponse = await axios(config); + return response.data; + } catch (error) { + throw this.handleError(error); + } + } + + /** + * Handle errors from API requests + * @param error The error object + * @returns The error message + */ + // eslint-disable-next-line class-methods-use-this + private handleError(error: any): Error { + if (axios.isAxiosError(error)) { + if (error.response) { + throw new Error(error.response.data.error_message || error.message); + } + throw new Error(error.message); + } + throw error; + } }