From 9b84425c8ca9e89888c8558a0813cda3b2bdddf2 Mon Sep 17 00:00:00 2001 From: pike Date: Tue, 16 Jan 2024 14:51:22 +0100 Subject: [PATCH 01/11] feat: Add youtube creds env.dist --- .env.dist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.dist b/.env.dist index 8576069..69b638a 100644 --- a/.env.dist +++ b/.env.dist @@ -73,3 +73,7 @@ FAIRPOST_REQUEST_PORT=8000 # reddit auth # FAIRPOST_REDDIT_ACCESS_TOKEN=xxx # FAIRPOST_REDDIT_REFRESH_TOKEN=xxx + +# youtube settings +# FAIRPOST_YOUTUBE_CLIENT_ID=xxx +# FAIRPOST_YOUTUBE_CLIENT_SECRET=xxx \ No newline at end of file From a950af42b9afdf602dfcd4636594996896493ab7 Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 10:45:14 +0100 Subject: [PATCH 02/11] feat: Add youtube , auth and test --- docs/Youtube.md | 64 +++++++++ src/platforms/YouTube/YouTube.ts | 128 ++++++++++++++++++ src/platforms/YouTube/YouTubeApi.ts | 124 +++++++++++++++++ src/platforms/YouTube/YouTubeAuth.ts | 190 +++++++++++++++++++++++++++ src/platforms/index.ts | 2 + 5 files changed, 508 insertions(+) create mode 100644 docs/Youtube.md create mode 100644 src/platforms/YouTube/YouTube.ts create mode 100644 src/platforms/YouTube/YouTubeApi.ts create mode 100644 src/platforms/YouTube/YouTubeAuth.ts diff --git a/docs/Youtube.md b/docs/Youtube.md new file mode 100644 index 0000000..e203792 --- /dev/null +++ b/docs/Youtube.md @@ -0,0 +1,64 @@ +# Platform: Youtube + +--The `youtube` platform manages a youtube **channel** + +## Setting up the Facebook platform + + +### Create a new project in your youtube account + + - Log in to Google Developers Console. + - Create a new project. + - set it to external, testing. only test users can use it + - Got to the project dashboard, currently at https://console.cloud.google.com/home/dashboard?project={yourproject} + - click Explore & Enable APIs. + - In the library, navigate to YouTube Data API v3 under YouTube APIs. + - enable that + - Create an OAuth consent screen + - website https://github.com/commonpike/fairpost + - privacy https://github.com/commonpike/fairpost/blob/develop/public/privacy-policy.md + - for the scopes, add YouTube Data API v3 + - Under credentials, create OAuth 2.0 Client IDs + - Save as `FAIRPOST_YOUTUBE_CLIENT_ID` and `FAIRPOST_YOUTUBE_CLIENT_SECRET` + +### Enable the platform + - Add 'youtube' to your `FAIRPOST_FEED_PLATFORMS` in `.env` + +### Get an OAuth2 Access Token for your platform + +This token last for a few hours and should be refreshed. +The refresh token (if given) lasts until it is revoked. + + - call `./fairpost.js setup-platform --platform=youtube` + - follow instructions from the command line + +### Test the platform + - call `./fairpost.js test-platform --platform=youtube` + +### Other settings + +## Manage additional pages with the same app + +... + +# Limitations + +## Images +### Supported Formats + + +### File Size + + +# Random documentation + +https://developers.google.com/youtube/v3 + +https://developers.google.com/youtube/v3/guides/auth/installed-apps#chrome + +https://blog.hubspot.com/website/how-to-get-youtube-api-key + +scopes +https://www.googleapis.com/auth/youtube.force-ssl +https://www.googleapis.com/auth/youtube.readonly +https://www.googleapis.com/auth/youtube.upload \ No newline at end of file diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts new file mode 100644 index 0000000..1489bc3 --- /dev/null +++ b/src/platforms/YouTube/YouTube.ts @@ -0,0 +1,128 @@ +//import * as fs from "fs"; +//import { handleApiError, handleEmptyResponse } from "../../utilities"; + +import Folder from "../../models/Folder"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import Post from "../../models/Post"; +//import Storage from "../../services/Storage"; +import YouTubeApi from "./YouTubeApi"; +import YouTubeAuth from "./YouTubeAuth"; + +export default class YouTube extends Platform { + id: PlatformId = PlatformId.YOUTUBE; + assetsFolder = "_youtube"; + postFileName = "post.json"; + + api: YouTubeApi; + auth: YouTubeAuth; + + constructor() { + super(); + this.api = new YouTubeApi(); + this.auth = new YouTubeAuth(); + } + + /** @inheritdoc */ + async setup() { + return await this.auth.setup(); + } + + /** @inheritdoc */ + async test() { + return this.getChannel(); + } + + /** @inheritdoc */ + async refresh(): Promise { + await this.auth.refresh(); + return true; + } + + /** @inheritdoc */ + async preparePost(folder: Folder): Promise { + Logger.trace("YouTube.preparePost", folder.id); + const post = await super.preparePost(folder); + if (post) { + // youtube: 1 video + post.limitFiles("video", 1); + post.removeFiles("image"); + post.removeFiles("text"); + post.removeFiles("other"); + if (!post.hasFiles("video")) { + post.valid = false; + } + post.save(); + } + return post; + } + + /** @inheritdoc */ + async publishPost(post: Post, dryrun: boolean = false): Promise { + Logger.trace("YouTube.publishPost", post.id, dryrun); + + let response = { id: "-99" } as { + id?: string; + headers?: { [key: string]: string }; + }; + let error = undefined as Error | undefined; + + try { + response = await this.publishVideoPost(post, dryrun); + } catch (e) { + error = e as Error; + } + + return post.processResult(response.id as string, "#unknown", { + date: new Date(), + dryrun: dryrun, + success: !error, + error: error, + response: response, + }); + } + + // Platform API Specific + + /** + * GET part of the channel snippet + * @returns object, incl. some ids and names + */ + private async getChannel() { + const result = (await this.api.get("channels", { + part: "snippet", + mine: "true", + })) as { + items?: { + id: string; + snippet: { + title: string; + customUrl: string; + }; + }[]; + }; + if (result.items?.length) { + return { + id: result.items[0].id, + snippet: { + title: result.items[0].snippet.title, + customUrl: result.items[0].snippet.customUrl, + }, + }; + } + } + + /** + * POST title & body & video to the posts endpoint using json + * + * untested. + * @param post + * @param dryrun + * @returns object, incl. id of the created post + */ + private async publishVideoPost(post: Post, dryrun: boolean = false) { + Logger.trace("YouTube.publishVideoPost", dryrun); + return { id: "-99" }; + } +} diff --git a/src/platforms/YouTube/YouTubeApi.ts b/src/platforms/YouTube/YouTubeApi.ts new file mode 100644 index 0000000..137fad7 --- /dev/null +++ b/src/platforms/YouTube/YouTubeApi.ts @@ -0,0 +1,124 @@ +import { + ApiResponseError, + handleApiError, + handleJsonResponse, +} from "../../utilities"; + +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +/** + * LinkedInApi: support for linkedin platform. + */ + +export default class YouTubeApi { + LGC_API_VERSION = "v2"; + API_VERSION = "v3"; + + /** + * Do a GET request on the api. + * @param endpoint - the path to call + * @param query - query string as object + */ + + public async get( + endpoint: string, + query: { [key: string]: string } = {}, + ): Promise { + // nb this is the legacy format + const url = new URL("https://www.googleapis.com"); + url.pathname = "/youtube/" + this.API_VERSION + "/" + endpoint; + url.search = new URLSearchParams(query).toString(); + + const accessToken = Storage.get("auth", "YOUTUBE_ACCESS_TOKEN"); + + Logger.trace("GET", url.href); + return await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Connection: "Keep-Alive", + Authorization: "Bearer " + accessToken, + "User-Agent": Storage.get("settings", "USER_AGENT"), + }, + }) + .then((res) => handleJsonResponse(res, true)) + .catch((err) => this.handleYouTubeError(err)) + .catch((err) => handleApiError(err)); + } + + /** + * Do a json POST request on the api. + * @param endpoint - the path to call + * @param body - body as object + + + public async postJson( + endpoint: string, + body = {}, + expectEmptyResponse = false, + ): Promise { + const url = new URL("https://api.linkedin.com"); + + const [pathname, search] = endpoint.split("?"); + url.pathname = "rest/" + pathname; + if (search) { + url.search = search; + } + const accessToken = Storage.get("auth", "LINKEDIN_ACCESS_TOKEN"); + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "Linkedin-Version": this.API_VERSION, + Authorization: "Bearer " + accessToken, + }, + body: JSON.stringify(body), + }) + .then((res) => + expectEmptyResponse + ? handleEmptyResponse(res, true) + : handleJsonResponse(res, true), + ) + .then((res) => { + const linkedinRes = res as { + id?: string; + headers?: { + "x-restli-id"?: string; + "x-linkedin-id"?: string; + }; + }; + if (!linkedinRes["id"] && "headers" in linkedinRes) { + if (linkedinRes.headers?.["x-restli-id"]) { + linkedinRes["id"] = linkedinRes.headers["x-restli-id"]; + } else if (linkedinRes.headers?.["x-linkedin-id"]) { + linkedinRes["id"] = linkedinRes.headers["x-linkedin-id"]; + } + } + return linkedinRes; + }) + .catch((err) => this.handleLinkedInError(err)) + .catch((err) => handleApiError(err)); + } + */ + + /** + * Handle api error + * + * Improve error message and rethrow it. + * @param error - ApiResponseError + */ + public async handleYouTubeError(error: ApiResponseError): Promise { + if (error.responseData) { + // + } + if (error.response?.headers) { + // + } + + throw error; + } +} diff --git a/src/platforms/YouTube/YouTubeAuth.ts b/src/platforms/YouTube/YouTubeAuth.ts new file mode 100644 index 0000000..947d0c7 --- /dev/null +++ b/src/platforms/YouTube/YouTubeAuth.ts @@ -0,0 +1,190 @@ +import { + ApiResponseError, + handleApiError, + handleJsonResponse, +} from "../../utilities"; + +import Logger from "../../services/Logger"; +import OAuth2Service from "../../services/OAuth2Service"; +import Storage from "../../services/Storage"; +import { strict as assert } from "assert"; + +export default class YouTubeAuth { + API_VERSION = "v2"; + + /** + * Set up LinkedIn platform + */ + async setup() { + const code = await this.requestCode(); + const tokens = await this.exchangeCode(code); + this.store(tokens); + } + + /** + * Refresh YouTube tokens + */ + async refresh() { + const tokens = (await this.post("accessToken", { + grant_type: "refresh_token", + refresh_token: Storage.get("auth", "YOUTUBE_REFRESH_TOKEN"), + client_id: Storage.get("settings", "YOUTUBE_CLIENT_ID"), + client_secret: Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), + })) as TokenResponse; + + if (!isTokenResponse(tokens)) { + throw Logger.error( + "YouTubeAuth.refresh: response is not a TokenResponse", + tokens, + ); + } + this.store(tokens); + } + + /** + * Request remote code using OAuth2Service + * @returns - code + */ + private async requestCode(): Promise { + Logger.trace("YouTubeAuth", "requestCode"); + const clientId = Storage.get("settings", "YOUTUBE_CLIENT_ID"); + const state = String(Math.random()).substring(2); + + // create auth url + const url = new URL("https://accounts.google.com"); + url.pathname = "o/oauth2/" + this.API_VERSION + "/auth"; + const query = { + client_id: clientId, + redirect_uri: OAuth2Service.getCallbackUrl(), + state: state, + response_type: "code", + duration: "permanent", + scope: [ + "https://www.googleapis.com/auth/youtube.force-ssl", + "https://www.googleapis.com/auth/youtube.readonly", + "https://www.googleapis.com/auth/youtube.upload", + ].join(" "), + }; + url.search = new URLSearchParams(query).toString(); + + const result = await OAuth2Service.requestRemotePermissions( + "YouTube", + url.href, + ); + if (result["error"]) { + const msg = result["error_reason"] + " - " + result["error_description"]; + throw Logger.error(msg, result); + } + if (result["state"] !== state) { + const msg = "Response state does not match request state"; + throw Logger.error(msg, result); + } + if (!result["code"]) { + const msg = "Remote response did not return a code"; + throw Logger.error(msg, result); + } + return result["code"] as string; + } + + /** + * Exchange remote code for tokens + * @param code - the code to exchange + * @returns - TokenResponse + */ + private async exchangeCode(code: string): Promise { + Logger.trace("YouTubeAuth", "exchangeCode", code); + const redirectUri = OAuth2Service.getCallbackUrl(); + + const tokens = (await this.post("token", { + grant_type: "authorization_code", + code: code, + client_id: Storage.get("settings", "YOUTUBE_CLIENT_ID"), + client_secret: Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), + redirect_uri: redirectUri, + })) as TokenResponse; + + if (!isTokenResponse(tokens)) { + throw Logger.error("Invalid TokenResponse", tokens); + } + + return tokens; + } + + /** + * Save all tokens in auth store + * @param tokens - the tokens to store + */ + private store(tokens: TokenResponse) { + Storage.set("auth", "YOUTUBE_ACCESS_TOKEN", tokens["access_token"]); + const accessExpiry = new Date( + new Date().getTime() + tokens["expires_in"] * 1000, + ).toISOString(); + Storage.set("auth", "YOUTUBE_ACCESS_EXPIRY", accessExpiry); + Storage.set("auth", "YOUTUBE_SCOPE", tokens["scope"]); + if ("refresh_token" in tokens) { + Storage.set( + "auth", + "YOUTUBE_REFRESH_TOKEN", + tokens["refresh_token"] ?? "", + ); + } + } + + // API implementation ------------------- + + /** + * Do a url-encoded POST request on the api. + * @param endpoint - the path to call + * @param body - body as object + */ + + private async post( + endpoint: string, + body: { [key: string]: string }, + ): Promise { + const url = new URL("https://oauth2.googleapis.com"); + url.pathname = endpoint; + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(body), + }) + .then((res) => handleJsonResponse(res)) + .catch((err) => this.handleYouTubeError(err)) + .catch((err) => handleApiError(err)); + } + + /** + * Handle api error + * + * Improve error message and rethrow it. + * @param error - ApiResponseError + */ + public async handleYouTubeError(error: ApiResponseError): Promise { + throw error; + } +} + +interface TokenResponse { + access_token: string; + token_type: "bearer"; + expires_in: number; + scope: string; + refresh_token?: string; +} + +function isTokenResponse(tokens: TokenResponse) { + try { + assert("access_token" in tokens); + assert("expires_in" in tokens); + assert("scope" in tokens); + } catch (e) { + return false; + } + return true; +} diff --git a/src/platforms/index.ts b/src/platforms/index.ts index e0efd0d..d408490 100644 --- a/src/platforms/index.ts +++ b/src/platforms/index.ts @@ -3,6 +3,7 @@ export { default as Instagram } from "./Instagram/Instagram"; export { default as Twitter } from "./Twitter/Twitter"; export { default as Reddit } from "./Reddit/Reddit"; export { default as LinkedIn } from "./LinkedIn/LinkedIn"; +export { default as YouTube } from "./YouTube/YouTube"; export { default as AsYouTube } from "./Ayrshare/AsYouTube"; export { default as AsInstagram } from "./Ayrshare/AsInstagram"; export { default as AsTwitter } from "./Ayrshare/AsTwitter"; @@ -18,6 +19,7 @@ export enum PlatformId { TWITTER = "twitter", REDDIT = "reddit", LINKEDIN = "linkedin", + YOUTUBE = "youtube", ASYOUTUBE = "asyoutube", ASINSTAGRAM = "asinstagram", ASFACEBOOK = "asfacebook", From 65d1236b52a0f711ec8657401ecbce73306da8e7 Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 10:47:41 +0100 Subject: [PATCH 03/11] fix: Refresh url --- src/platforms/YouTube/YouTubeAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platforms/YouTube/YouTubeAuth.ts b/src/platforms/YouTube/YouTubeAuth.ts index 947d0c7..7e765b3 100644 --- a/src/platforms/YouTube/YouTubeAuth.ts +++ b/src/platforms/YouTube/YouTubeAuth.ts @@ -25,7 +25,7 @@ export default class YouTubeAuth { * Refresh YouTube tokens */ async refresh() { - const tokens = (await this.post("accessToken", { + const tokens = (await this.post("token", { grant_type: "refresh_token", refresh_token: Storage.get("auth", "YOUTUBE_REFRESH_TOKEN"), client_id: Storage.get("settings", "YOUTUBE_CLIENT_ID"), From ddffb643ce5f11a23405d18b40471aa26fe1d862 Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 10:53:01 +0100 Subject: [PATCH 04/11] chore: Remove lgc comments --- src/platforms/YouTube/YouTubeApi.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/platforms/YouTube/YouTubeApi.ts b/src/platforms/YouTube/YouTubeApi.ts index 137fad7..bae61db 100644 --- a/src/platforms/YouTube/YouTubeApi.ts +++ b/src/platforms/YouTube/YouTubeApi.ts @@ -12,7 +12,6 @@ import Storage from "../../services/Storage"; */ export default class YouTubeApi { - LGC_API_VERSION = "v2"; API_VERSION = "v3"; /** @@ -25,7 +24,6 @@ export default class YouTubeApi { endpoint: string, query: { [key: string]: string } = {}, ): Promise { - // nb this is the legacy format const url = new URL("https://www.googleapis.com"); url.pathname = "/youtube/" + this.API_VERSION + "/" + endpoint; url.search = new URLSearchParams(query).toString(); From 49e65e52bb65f08b5c54531a47c2a9f30c9714ee Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 12:48:04 +0100 Subject: [PATCH 05/11] doc: How to add a platform --- docs/Add-a-platform.md | 289 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 docs/Add-a-platform.md diff --git a/docs/Add-a-platform.md b/docs/Add-a-platform.md new file mode 100644 index 0000000..f56aa79 --- /dev/null +++ b/docs/Add-a-platform.md @@ -0,0 +1,289 @@ +# How to add a new platform + +If your platform is not yet supported by Fairpost, +you can write your own code to support it. + +## Minimal setup + +To add support for a new platform, add a class to `src/platforms` +extending `src/classes/Platform`. You want to override at least the +method `preparePost(folder)` and `publishPost(post,dryrun)`. + +Make sure not to throw errors in or below publishPost; instead, just +return false and let the Post.processResult(). + +```php + { + const post = await super.preparePost(folder); + if (post) { + // prepare your post here + post.save(); + } + return post; + } + + /** @inheritdoc */ + async publishPost(post: Post, dryrun: boolean = false): Promise { + + let response = { id: "-99" } as { id: string }; + let error = undefined as Error | undefined; + + try { + response = await this.publishFooBarPost(post, dryrun); + } catch (e) { + error = e as Error; + } + + return post.processResult( + response.id, + "https://url-to-your-post", + { + date: new Date(), + dryrun: dryrun, + success: !error, + response: response, + error: error, + }, + ); + } + + async publishFooBarPost(post: Post, dryrun: boolean = false): object { + return { + id: "-99", + error: "not implemented" + } + } +} + +``` + +Then in `src/platforms/index.ts` +- import your class +- add `PlatformId.FOOBAR` for your platform + +Then in `.env`, enable your platformId +``` +FAIRPOST_PLATFORMS=foobar,..,.. +``` + +check if it works: +``` +npm run lint:fix # optional +npm run build +./fairpost.js get-platforms +``` + +and party. + +### Add more methods + +#### FooBar.test() + +This method allows you to call `fairpost.js test-platform --platform=foobar`. +You can return anything. + +#### FooBar.setup() + +This method allows you to call `fairpost.js setup-platform --platform=foobar`, +usually to get the access tokens and save them in Storage. + +#### FooBar.refresh() + +This method allows you to call `fairpost.js refresh-platform --platform=foobar`, +usually to refresh the access tokens and save them in Storage. + +### Using Storage + +There are two stores, `settings` and `auth`. Depending on your +configuration, these may be stored in different places. If you +storage uses `.env`, it is read-only. + +```php + { + + return await fetch(url, { + method: "GET", + headers: { + Bla: 'Bla' + }, + }) + .then((res) => handleJsonResponse(res)) + .catch((err) => this.handleFooBarError(err)) + .catch((err) => handleApiError(err)); + +... + + private async handleFooBarError(error: ApiResponseError): Promise { + error.message += '; FooBar made a booboo' + throw error; + } + +``` + +### FooBarAuth.ts + + +Another good approach to refactor is to take the Authentication +flow out of your platform into a separate `FooBarAuth.ts`. +Add a method `setup()` and link your `Foobar.setup()` there. +Optionally add a method `refresh()` and link your `Foobar.refresh()` there. + +There is a service to help you with the OAuth flow. It starts a web server +and presents you with a link to click, and processes the response: + +```php + { + const clientId = Storage.get("settings", "FOOBAR_CLIENT_ID"); + const state = String(Math.random()).substring(2); + + // create auth url + const url = new URL("https://foobar.com"); + url.pathname = "bla/auth"; + const query = { + client_id: clientId, + redirect_uri: OAuth2Service.getCallbackUrl(), + state: state, + response_type: "code", + scope: [ + "foo", + "bar" + ].join(" "), + }; + url.search = new URLSearchParams(query).toString(); + + const result = await OAuth2Service.requestRemotePermissions( + "FooBar", + url.href, + ); + if (result["error"]) { + const msg = result["error_reason"] + " - " + result["error_description"]; + throw Logger.error(msg, result); + } + if (result["state"] !== state) { + const msg = "Response state does not match request state"; + throw Logger.error(msg, result); + } + if (!result["code"]) { + const msg = "Remote response did not return a code"; + throw Logger.error(msg, result); + } + return result["code"] as string; + } + /** + * Exchange remote code for tokens + * @param code - the code to exchange + * @returns - TokenResponse + */ + private async exchangeCode(code: string): Promise { + const redirectUri = OAuth2Service.getCallbackUrl(); + const tokens = (await this.post("token", { + grant_type: "authorization_code", + code: code, + client_id: Storage.get("settings", "FOOBAR_CLIENT_ID"), + client_secret: Storage.get("settings", "FOOBAR_CLIENT_SECRET"), + redirect_uri: redirectUri, + })); + if (!('accessToken' in tokens)) { + throw Logger.error("Invalid TokenResponse", tokens); + } + + return tokens; + } + + /** + * Save all tokens in auth store + * @param tokens - the tokens to store + */ + private store(tokens: TokenResponse) { + Storage.set("auth", "FOOBAR_ACCESS_TOKEN", tokens["access_token"]); + } + + // a very minimal tokenresponse + interface TokenResponse { + access_token: string; + } + +``` \ No newline at end of file From 0f96afec1326b06c7d0d60b9745fb9625cd06c68 Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 12:58:24 +0100 Subject: [PATCH 06/11] fix: Add the post in the platform doc --- docs/Add-a-platform.md | 32 +++++++++++++++++++++++++++- src/platforms/YouTube/YouTubeAuth.ts | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/Add-a-platform.md b/docs/Add-a-platform.md index f56aa79..3887435 100644 --- a/docs/Add-a-platform.md +++ b/docs/Add-a-platform.md @@ -199,6 +199,11 @@ and presents you with a link to click, and processes the response: import OAuth2Service from "../../services/OAuth2Service"; import Logger from "../../services/Logger"; import Storage from "../../services/Storage"; +import { + ApiResponseError, + handleApiError, + handleJsonResponse, +} from "../../utilities"; export default class FooBarAuth { @@ -281,7 +286,32 @@ export default class FooBarAuth { Storage.set("auth", "FOOBAR_ACCESS_TOKEN", tokens["access_token"]); } - // a very minimal tokenresponse + /** + * The oauth post is sometimes slightly different + * from the regular api post .. + */ + private async post( + endpoint: string, + body: { [key: string]: string }, + ): Promise { + const url = new URL("https://foobar.com"); + url.pathname = "bla/auth/"+endpoint; + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(body), + }) + .then((res) => handleJsonResponse(res)) + .catch((err) => handleApiError(err)); + } + + /** + * A very minimal TokenResponse. Extend to suit your needs. + */ interface TokenResponse { access_token: string; } diff --git a/src/platforms/YouTube/YouTubeAuth.ts b/src/platforms/YouTube/YouTubeAuth.ts index 7e765b3..68e5e77 100644 --- a/src/platforms/YouTube/YouTubeAuth.ts +++ b/src/platforms/YouTube/YouTubeAuth.ts @@ -13,7 +13,7 @@ export default class YouTubeAuth { API_VERSION = "v2"; /** - * Set up LinkedIn platform + * Set up YouTube platform */ async setup() { const code = await this.requestCode(); From c5f85258329dd9f584045023bd7b09a415ca8acf Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 13:02:16 +0100 Subject: [PATCH 07/11] doc: Improve --- docs/Add-a-platform.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Add-a-platform.md b/docs/Add-a-platform.md index 3887435..a9f849a 100644 --- a/docs/Add-a-platform.md +++ b/docs/Add-a-platform.md @@ -186,9 +186,11 @@ import { Another good approach to refactor is to take the Authentication -flow out of your platform into a separate `FooBarAuth.ts`. +flow out of your platform into a separate `FooBar/FooBarAuth.ts`. Add a method `setup()` and link your `Foobar.setup()` there. Optionally add a method `refresh()` and link your `Foobar.refresh()` there. +Store the access tokens in `auth` Storage, so you can access them +in your platform class. There is a service to help you with the OAuth flow. It starts a web server and presents you with a link to click, and processes the response: From 20d7ee645a739e26b40454ae7959745320735791 Mon Sep 17 00:00:00 2001 From: pike Date: Thu, 18 Jan 2024 13:04:38 +0100 Subject: [PATCH 08/11] doc: Improve --- docs/Youtube.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Youtube.md b/docs/Youtube.md index e203792..ed7507a 100644 --- a/docs/Youtube.md +++ b/docs/Youtube.md @@ -2,10 +2,10 @@ --The `youtube` platform manages a youtube **channel** -## Setting up the Facebook platform +## Setting up the YouTube platform -### Create a new project in your youtube account +### Create a new project in your account - Log in to Google Developers Console. - Create a new project. From 067f5caf3c72f8281e7b2e6cc64c0b8d09abc333 Mon Sep 17 00:00:00 2001 From: pike Date: Sun, 21 Jan 2024 12:49:11 +0100 Subject: [PATCH 09/11] feat: Update YouTube and YouTubeAuth to use google libs --- README.md | 2 + docs/{Add-a-platform.md => NewPlatform.md} | 5 +- docs/Youtube.md | 47 +- package-lock.json | 640 +++++++++++++++++++++ package.json | 2 + public/privacy-policy.md | 4 + src/platforms/YouTube/YouTube.ts | 38 +- src/platforms/YouTube/YouTubeAuth.ts | 157 ++--- 8 files changed, 774 insertions(+), 121 deletions(-) rename docs/{Add-a-platform.md => NewPlatform.md} (98%) diff --git a/README.md b/README.md index 61d872c..50328ad 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,8 @@ in `src/platforms/index.ts` and enable your platformId in your `.env`. Similarly, you can copy one platform, rename it and edit it to your likings, give it a different `platformId` and enable that. +For more detailed instructions look at [How to add a new platform](./docs/NewPlatform.md) + Oh, and send me a PR if you create anything useful :-) diff --git a/docs/Add-a-platform.md b/docs/NewPlatform.md similarity index 98% rename from docs/Add-a-platform.md rename to docs/NewPlatform.md index a9f849a..6986224 100644 --- a/docs/Add-a-platform.md +++ b/docs/NewPlatform.md @@ -143,8 +143,9 @@ move your class there, and update the imports. ### FooBarApi.ts -A simple approach to refactor is to take the API -calls out of your platform into a separate `FooBar/FooBarApi.ts`. +If you're using your own api calls, a simple approach to +refactor is to take these API calls out of your platform +into a separate `FooBar/FooBarApi.ts`. There are some utilities to help you with your Api responses and errors. The below code will unpack a json diff --git a/docs/Youtube.md b/docs/Youtube.md index ed7507a..db63617 100644 --- a/docs/Youtube.md +++ b/docs/Youtube.md @@ -1,16 +1,28 @@ -# Platform: Youtube +# Platform: YouTube ---The `youtube` platform manages a youtube **channel** +The `youtube` platform manages a youtube **channel** +using `@googleapis/youtube` and `google-auth-library`. + +To upload public videos, your app needs to be verified / audited first. + +By using Fairpost on YouTube, you are agreeing to be bound by +the YouTube Terms of Service: https://www.youtube.com/t/terms + +Your posts will be preprocessed to fit YouTube. The limitations +imposed by Fairpost are not imposed by YouTube. ## Setting up the YouTube platform ### Create a new project in your account - - Log in to Google Developers Console. +Google has a wizard to create a youtube app: https://console.developers.google.com/start/api?id=youtube +Below is how to do it manually. + + - Log in to Google Developers Console: https://console.cloud.google.com/cloud-resource-manager - Create a new project. - set it to external, testing. only test users can use it - - Got to the project dashboard, currently at https://console.cloud.google.com/home/dashboard?project={yourproject} + - Go to the project dashboard, currently at https://console.cloud.google.com/home/dashboard?project={yourproject} - click Explore & Enable APIs. - In the library, navigate to YouTube Data API v3 under YouTube APIs. - enable that @@ -35,6 +47,17 @@ The refresh token (if given) lasts until it is revoked. ### Test the platform - call `./fairpost.js test-platform --platform=youtube` +### Get your app audited + +To have Fairpost publish **public** videos, your app has to be audited + + - go to https://support.google.com/youtube/contact/yt_api_form + - request an audit + - For the website, link to https://github.com/commonpike/fairpost + - For the 'document describing your implementation', post this file + - wait. + + ### Other settings ## Manage additional pages with the same app @@ -43,7 +66,7 @@ The refresh token (if given) lasts until it is revoked. # Limitations -## Images +## Video ### Supported Formats @@ -54,6 +77,10 @@ The refresh token (if given) lasts until it is revoked. https://developers.google.com/youtube/v3 +https://developers.google.com/youtube/v3/docs/videos/insert + +https://developers.google.com/youtube/v3/docs/videos#resource + https://developers.google.com/youtube/v3/guides/auth/installed-apps#chrome https://blog.hubspot.com/website/how-to-get-youtube-api-key @@ -61,4 +88,12 @@ https://blog.hubspot.com/website/how-to-get-youtube-api-key scopes https://www.googleapis.com/auth/youtube.force-ssl https://www.googleapis.com/auth/youtube.readonly -https://www.googleapis.com/auth/youtube.upload \ No newline at end of file +https://www.googleapis.com/auth/youtube.upload + +https://googleapis.dev/nodejs/googleapis/latest/slides/ + +https://pixelswap.fr/entry/how-to-upload-a-video-on-youtube-with-nodejs/ + +https://stackoverflow.com/questions/65258438/how-to-upload-video-to-youtube-using-google-api-without-libraries + +https://developers.google.com/youtube/terms/required-minimum-functionality \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1a86f1c..e9fb7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "1.2.0", "license": "ISC", "dependencies": { + "@googleapis/youtube": "^13.0.0", "dotenv": "^16.0.3", "fast-xml-parser": "^4.3.2", + "google-auth-library": "^9.4.2", "log4js": "^6.9.1", "node-fetch": "^2.6.7", "sharp": "0.33.1", @@ -141,6 +143,17 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@googleapis/youtube": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/youtube/-/youtube-13.0.0.tgz", + "integrity": "sha512-txgO03TGMXLEcNEt7wE/kMzskoTbGp8P1wAR70B0VPTs0aTKh1povl2o8Ut4p2fCx14ITC6MWIgl05J4+btAJg==", + "dependencies": { + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -949,6 +962,17 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1025,6 +1049,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -1034,6 +1077,14 @@ "node": ">=0.6" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -1068,6 +1119,11 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -1095,6 +1151,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1256,6 +1325,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -1320,6 +1402,14 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1560,6 +1650,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1720,6 +1815,65 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1799,6 +1953,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.2.tgz", + "integrity": "sha512-rTLO4gjhqqo3WvYKL5IdtlCvRqeQ4hxUx/p4lObobY2xotFW3bCQC+Qf1N51CYOfiqfMecdMwW9RIo7dFWYjqw==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz", + "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.0.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1810,6 +2007,18 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1819,6 +2028,62 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -2036,6 +2301,14 @@ "node": ">=12.0.0" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2062,6 +2335,25 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2246,6 +2538,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2446,6 +2746,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2632,6 +2946,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2646,6 +2979,21 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/sharp": { "version": "0.33.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", @@ -2706,6 +3054,19 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -2991,6 +3352,23 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -3141,6 +3519,14 @@ "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true }, + "@googleapis/youtube": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/youtube/-/youtube-13.0.0.tgz", + "integrity": "sha512-txgO03TGMXLEcNEt7wE/kMzskoTbGp8P1wAR70B0VPTs0aTKh1povl2o8Ut4p2fCx14ITC6MWIgl05J4+btAJg==", + "requires": { + "googleapis-common": "^7.0.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -3531,6 +3917,14 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3588,12 +3982,22 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true }, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + }, "bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -3622,6 +4026,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -3637,6 +4046,16 @@ "run-applescript": "^5.0.0" } }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3754,6 +4173,16 @@ "untildify": "^4.0.0" } }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -3794,6 +4223,14 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3953,6 +4390,11 @@ "strip-final-newline": "^3.0.0" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4078,6 +4520,49 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + } + } + }, + "gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4130,6 +4615,40 @@ "slash": "^3.0.0" } }, + "google-auth-library": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.2.tgz", + "integrity": "sha512-rTLO4gjhqqo3WvYKL5IdtlCvRqeQ4hxUx/p4lObobY2xotFW3bCQC+Qf1N51CYOfiqfMecdMwW9RIo7dFWYjqw==", + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + } + }, + "googleapis-common": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz", + "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.0.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4141,12 +4660,56 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "gtoken": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, "human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -4291,6 +4854,14 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4317,6 +4888,25 @@ "graceful-fs": "^4.1.6" } }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4450,6 +5040,11 @@ } } }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4584,6 +5179,14 @@ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4695,6 +5298,11 @@ "queue-microtask": "^1.2.2" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4703,6 +5311,18 @@ "lru-cache": "^6.0.0" } }, + "set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "requires": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, "sharp": { "version": "0.33.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", @@ -4747,6 +5367,16 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -4950,6 +5580,16 @@ "punycode": "^2.1.0" } }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index da6ed39..4f5e207 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,10 @@ "author": "codepike", "license": "ISC", "dependencies": { + "@googleapis/youtube": "^13.0.0", "dotenv": "^16.0.3", "fast-xml-parser": "^4.3.2", + "google-auth-library": "^9.4.2", "log4js": "^6.9.1", "node-fetch": "^2.6.7", "sharp": "0.33.1", diff --git a/public/privacy-policy.md b/public/privacy-policy.md index 448d1d7..84456ed 100644 --- a/public/privacy-policy.md +++ b/public/privacy-policy.md @@ -7,6 +7,10 @@ This Privacy Policy describes The Softwares policies and procedures on the colle The Software can use Your Personal data to provide the service. By using Fairpost, You agree to the collection and use of information in accordance with this Privacy Policy. +By using Fairpost for any of the following platforms, you agree to the privacy policies defined for that platform: + +- YouTube: http://www.google.com/policies/privacy + Interpretation and Definitions ------------------------------ diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 1489bc3..04c12f7 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -6,8 +6,6 @@ import Logger from "../../services/Logger"; import Platform from "../../models/Platform"; import { PlatformId } from ".."; import Post from "../../models/Post"; -//import Storage from "../../services/Storage"; -import YouTubeApi from "./YouTubeApi"; import YouTubeAuth from "./YouTubeAuth"; export default class YouTube extends Platform { @@ -15,12 +13,10 @@ export default class YouTube extends Platform { assetsFolder = "_youtube"; postFileName = "post.json"; - api: YouTubeApi; auth: YouTubeAuth; constructor() { super(); - this.api = new YouTubeApi(); this.auth = new YouTubeAuth(); } @@ -90,27 +86,33 @@ export default class YouTube extends Platform { * @returns object, incl. some ids and names */ private async getChannel() { - const result = (await this.api.get("channels", { - part: "snippet", - mine: "true", + const client = this.auth.getClient(); + const result = (await client.channels.list({ + part: ["snippet", "contentDetails", "status"], + mine: true, })) as { - items?: { - id: string; - snippet: { - title: string; - customUrl: string; - }; - }[]; + data?: { + items?: { + id: string; + snippet: { + title: string; + customUrl: string; + }; + }[]; + }; + status: number; + statusText: string; }; - if (result.items?.length) { + if (result.data?.items?.length) { return { - id: result.items[0].id, + id: result.data.items[0].id, snippet: { - title: result.items[0].snippet.title, - customUrl: result.items[0].snippet.customUrl, + title: result.data.items[0].snippet.title, + customUrl: result.data.items[0].snippet.customUrl, }, }; } + throw Logger.error("YouTube.getChannel", "invalid result", result); } /** diff --git a/src/platforms/YouTube/YouTubeAuth.ts b/src/platforms/YouTube/YouTubeAuth.ts index 68e5e77..e752207 100644 --- a/src/platforms/YouTube/YouTubeAuth.ts +++ b/src/platforms/YouTube/YouTubeAuth.ts @@ -1,16 +1,12 @@ -import { - ApiResponseError, - handleApiError, - handleJsonResponse, -} from "../../utilities"; - import Logger from "../../services/Logger"; +import { OAuth2Client } from "google-auth-library"; import OAuth2Service from "../../services/OAuth2Service"; import Storage from "../../services/Storage"; import { strict as assert } from "assert"; +import { youtube_v3 } from "@googleapis/youtube"; export default class YouTubeAuth { - API_VERSION = "v2"; + client?: youtube_v3.Youtube; /** * Set up YouTube platform @@ -25,20 +21,37 @@ export default class YouTubeAuth { * Refresh YouTube tokens */ async refresh() { - const tokens = (await this.post("token", { - grant_type: "refresh_token", + const auth = new OAuth2Client( + Storage.get("settings", "YOUTUBE_CLIENT_ID"), + Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), + ); + auth.setCredentials({ refresh_token: Storage.get("auth", "YOUTUBE_REFRESH_TOKEN"), - client_id: Storage.get("settings", "YOUTUBE_CLIENT_ID"), - client_secret: Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), - })) as TokenResponse; + }); + const response = (await auth.getAccessToken()) as { + res: { data: TokenResponse }; + }; + if (isTokenResponse(response["res"]["data"])) { + this.store(response["res"]["data"]); + return; + } + throw Logger.error("YouTubeAuth.refresh", "no a valid repsonse", response); + } - if (!isTokenResponse(tokens)) { - throw Logger.error( - "YouTubeAuth.refresh: response is not a TokenResponse", - tokens, - ); + /** + * Get or create a YouTube client + * @returns - youtube_v3.Youtube + */ + public getClient(): youtube_v3.Youtube { + if (this.client) { + return this.client; } - this.store(tokens); + const auth = new OAuth2Client(); + auth.setCredentials({ + access_token: Storage.get("auth", "YOUTUBE_ACCESS_TOKEN"), + }); + this.client = new youtube_v3.Youtube({ auth }); + return this.client; } /** @@ -47,30 +60,24 @@ export default class YouTubeAuth { */ private async requestCode(): Promise { Logger.trace("YouTubeAuth", "requestCode"); - const clientId = Storage.get("settings", "YOUTUBE_CLIENT_ID"); const state = String(Math.random()).substring(2); - // create auth url - const url = new URL("https://accounts.google.com"); - url.pathname = "o/oauth2/" + this.API_VERSION + "/auth"; - const query = { - client_id: clientId, - redirect_uri: OAuth2Service.getCallbackUrl(), - state: state, - response_type: "code", - duration: "permanent", + const auth = new OAuth2Client( + Storage.get("settings", "YOUTUBE_CLIENT_ID"), + Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), + OAuth2Service.getCallbackUrl(), + ); + const url = auth.generateAuthUrl({ + access_type: "offline", scope: [ "https://www.googleapis.com/auth/youtube.force-ssl", "https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/youtube.upload", - ].join(" "), - }; - url.search = new URLSearchParams(query).toString(); + ], + state: state, + }); - const result = await OAuth2Service.requestRemotePermissions( - "YouTube", - url.href, - ); + const result = await OAuth2Service.requestRemotePermissions("YouTube", url); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; throw Logger.error(msg, result); @@ -93,21 +100,20 @@ export default class YouTubeAuth { */ private async exchangeCode(code: string): Promise { Logger.trace("YouTubeAuth", "exchangeCode", code); - const redirectUri = OAuth2Service.getCallbackUrl(); - const tokens = (await this.post("token", { - grant_type: "authorization_code", - code: code, - client_id: Storage.get("settings", "YOUTUBE_CLIENT_ID"), - client_secret: Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), - redirect_uri: redirectUri, - })) as TokenResponse; + const auth = new OAuth2Client( + Storage.get("settings", "YOUTUBE_CLIENT_ID"), + Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), + OAuth2Service.getCallbackUrl(), + ); - if (!isTokenResponse(tokens)) { - throw Logger.error("Invalid TokenResponse", tokens); + const response = (await auth.getToken(code)) as { + tokens: TokenResponse; + }; + if (!isTokenResponse(response.tokens)) { + throw Logger.error("Invalid TokenResponse", response.tokens); } - - return tokens; + return response.tokens; } /** @@ -116,11 +122,11 @@ export default class YouTubeAuth { */ private store(tokens: TokenResponse) { Storage.set("auth", "YOUTUBE_ACCESS_TOKEN", tokens["access_token"]); - const accessExpiry = new Date( - new Date().getTime() + tokens["expires_in"] * 1000, - ).toISOString(); + const accessExpiry = new Date(tokens["expiry_date"]).toISOString(); Storage.set("auth", "YOUTUBE_ACCESS_EXPIRY", accessExpiry); - Storage.set("auth", "YOUTUBE_SCOPE", tokens["scope"]); + if ("scope" in tokens) { + Storage.set("auth", "YOUTUBE_SCOPE", tokens["scope"] ?? ""); + } if ("refresh_token" in tokens) { Storage.set( "auth", @@ -129,60 +135,21 @@ export default class YouTubeAuth { ); } } - - // API implementation ------------------- - - /** - * Do a url-encoded POST request on the api. - * @param endpoint - the path to call - * @param body - body as object - */ - - private async post( - endpoint: string, - body: { [key: string]: string }, - ): Promise { - const url = new URL("https://oauth2.googleapis.com"); - url.pathname = endpoint; - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams(body), - }) - .then((res) => handleJsonResponse(res)) - .catch((err) => this.handleYouTubeError(err)) - .catch((err) => handleApiError(err)); - } - - /** - * Handle api error - * - * Improve error message and rethrow it. - * @param error - ApiResponseError - */ - public async handleYouTubeError(error: ApiResponseError): Promise { - throw error; - } } interface TokenResponse { access_token: string; - token_type: "bearer"; - expires_in: number; - scope: string; + token_type?: "bearer"; + expiry_date: number; refresh_token?: string; + scope?: string; + id_token?: string; } function isTokenResponse(tokens: TokenResponse) { try { assert("access_token" in tokens); - assert("expires_in" in tokens); - assert("scope" in tokens); + assert("expiry_date" in tokens); } catch (e) { return false; } From d28c5268ca91573301f69668e839918bd42d8bb3 Mon Sep 17 00:00:00 2001 From: pike Date: Sun, 21 Jan 2024 21:39:44 +0100 Subject: [PATCH 10/11] feat: Tested and working upload YouTube --- .env.dist | 4 +- docs/NewPlatform.md | 40 ++----------- docs/Youtube.md | 8 ++- src/platforms/YouTube/YouTube.ts | 98 ++++++++++++++++++++++++++++---- 4 files changed, 101 insertions(+), 49 deletions(-) diff --git a/.env.dist b/.env.dist index 9775ed0..67e8aa7 100644 --- a/.env.dist +++ b/.env.dist @@ -77,4 +77,6 @@ FAIRPOST_REQUEST_PORT=8000 # youtube settings # FAIRPOST_YOUTUBE_CLIENT_ID=xxx -# FAIRPOST_YOUTUBE_CLIENT_SECRET=xxx \ No newline at end of file +# FAIRPOST_YOUTUBE_CLIENT_SECRET=xxx +# FAIRPOST_YOUTUBE_PRIVACY=public +# FAIRPOST_YOUTUBE_CATEGORY=test \ No newline at end of file diff --git a/docs/NewPlatform.md b/docs/NewPlatform.md index 6986224..9f54b9a 100644 --- a/docs/NewPlatform.md +++ b/docs/NewPlatform.md @@ -202,11 +202,6 @@ and presents you with a link to click, and processes the response: import OAuth2Service from "../../services/OAuth2Service"; import Logger from "../../services/Logger"; import Storage from "../../services/Storage"; -import { - ApiResponseError, - handleApiError, - handleJsonResponse, -} from "../../utilities"; export default class FooBarAuth { @@ -265,8 +260,9 @@ export default class FooBarAuth { * @param code - the code to exchange * @returns - TokenResponse */ - private async exchangeCode(code: string): Promise { + private async exchangeCode(code: string) { const redirectUri = OAuth2Service.getCallbackUrl(); + // implement your own post method ... const tokens = (await this.post("token", { grant_type: "authorization_code", code: code, @@ -285,38 +281,10 @@ export default class FooBarAuth { * Save all tokens in auth store * @param tokens - the tokens to store */ - private store(tokens: TokenResponse) { + private store(tokens) { Storage.set("auth", "FOOBAR_ACCESS_TOKEN", tokens["access_token"]); } - /** - * The oauth post is sometimes slightly different - * from the regular api post .. - */ - private async post( - endpoint: string, - body: { [key: string]: string }, - ): Promise { - const url = new URL("https://foobar.com"); - url.pathname = "bla/auth/"+endpoint; - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams(body), - }) - .then((res) => handleJsonResponse(res)) - .catch((err) => handleApiError(err)); - } - - /** - * A very minimal TokenResponse. Extend to suit your needs. - */ - interface TokenResponse { - access_token: string; - } +} ``` \ No newline at end of file diff --git a/docs/Youtube.md b/docs/Youtube.md index db63617..0cb27a6 100644 --- a/docs/Youtube.md +++ b/docs/Youtube.md @@ -60,6 +60,9 @@ To have Fairpost publish **public** videos, your app has to be audited ### Other settings +- `FAIRPOST_YOUTUBE_PRIVACY` = public | private +- `FAIRPOST_YOUTUBE_CATEGORY` = valid youtube category id + ## Manage additional pages with the same app ... @@ -67,10 +70,13 @@ To have Fairpost publish **public** videos, your app has to be audited # Limitations ## Video -### Supported Formats +### Supported Formats +Accepted Media MIME types: +video/*, application/octet-stream ### File Size +Maximum file size: 256GB # Random documentation diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 04c12f7..9b44c14 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -1,11 +1,11 @@ -//import * as fs from "fs"; -//import { handleApiError, handleEmptyResponse } from "../../utilities"; +import * as fs from "fs"; import Folder from "../../models/Folder"; import Logger from "../../services/Logger"; import Platform from "../../models/Platform"; import { PlatformId } from ".."; import Post from "../../models/Post"; +import Storage from "../../services/Storage"; import YouTubeAuth from "./YouTubeAuth"; export default class YouTube extends Platform { @@ -15,6 +15,16 @@ export default class YouTube extends Platform { auth: YouTubeAuth; + // post defaults + notifySubscribers = true; + onBehalfOfContentOwner = ""; + onBehalfOfContentOwnerChannel = ""; + defaultLanguage = "en-us"; + embeddable = true; + license = "youtube"; + publicStatsViewable = true; + selfDeclaredMadeForKids = false; + constructor() { super(); this.auth = new YouTubeAuth(); @@ -60,7 +70,6 @@ export default class YouTube extends Platform { let response = { id: "-99" } as { id?: string; - headers?: { [key: string]: string }; }; let error = undefined as Error | undefined; @@ -70,13 +79,17 @@ export default class YouTube extends Platform { error = e as Error; } - return post.processResult(response.id as string, "#unknown", { - date: new Date(), - dryrun: dryrun, - success: !error, - error: error, - response: response, - }); + return post.processResult( + response.id as string, + "https://www.youtube.com/watch?v=" + response.id, + { + date: new Date(), + dryrun: dryrun, + success: !error, + error: error, + response: response, + }, + ); } // Platform API Specific @@ -125,6 +138,69 @@ export default class YouTube extends Platform { */ private async publishVideoPost(post: Post, dryrun: boolean = false) { Logger.trace("YouTube.publishVideoPost", dryrun); - return { id: "-99" }; + + const file = post.getFiles("video")[0]; + + const client = this.auth.getClient(); + Logger.trace("YouTube.publishVideoPost", "uploading " + file.name + " ..."); + const result = (await client.videos.insert({ + part: ["snippet", "status"], + notifySubscribers: this.notifySubscribers, + ...(this.onBehalfOfContentOwner && { + onBehalfOfContentOwner: this.onBehalfOfContentOwner, + }), + ...(this.onBehalfOfContentOwnerChannel && { + onBehalfOfContentOwnerChannel: this.onBehalfOfContentOwnerChannel, + }), + requestBody: { + snippet: { + title: post.title, + description: post.getCompiledBody("!title"), + tags: post.tags, // both in body and separate + categoryId: Storage.get("settings", "YOUTUBE_CATEGORY", ""), + defaultLanguage: this.defaultLanguage, + }, + status: { + embeddable: this.embeddable, + license: this.license, + publicStatsViewable: this.publicStatsViewable, + selfDeclaredMadeForKids: this.selfDeclaredMadeForKids, + privacyStatus: Storage.get("settings", "YOUTUBE_PRIVACY"), + }, + }, + media: { + mimeType: file.mimetype, + body: fs.createReadStream(post.getFilePath(file.name)), + }, + })) as { + data: { + id: string; + status?: { + uploadStatus: string; + failureReason: string; + rejectionReason: string; + }; + snippet: object; + }; + }; + + if (result.data.status?.uploadStatus !== "uploaded") { + throw Logger.error( + "YouTube.publishVideoPost", + "failed", + result.data.status?.uploadStatus, + result.data.status?.failureReason, + result.data.status?.rejectionReason, + ); + } + if (!result.data.id) { + throw Logger.error( + "YouTube.publishVideoPost", + "missing id in result", + result, + ); + } + + return { id: result.data.id }; } } From f054eefa691d8b0962266243ee2d21a64dfdd9c4 Mon Sep 17 00:00:00 2001 From: pike Date: Sun, 21 Jan 2024 21:44:13 +0100 Subject: [PATCH 11/11] fix: We dont need an api for youtube --- src/platforms/YouTube/YouTubeApi.ts | 122 ---------------------------- 1 file changed, 122 deletions(-) delete mode 100644 src/platforms/YouTube/YouTubeApi.ts diff --git a/src/platforms/YouTube/YouTubeApi.ts b/src/platforms/YouTube/YouTubeApi.ts deleted file mode 100644 index bae61db..0000000 --- a/src/platforms/YouTube/YouTubeApi.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - ApiResponseError, - handleApiError, - handleJsonResponse, -} from "../../utilities"; - -import Logger from "../../services/Logger"; -import Storage from "../../services/Storage"; - -/** - * LinkedInApi: support for linkedin platform. - */ - -export default class YouTubeApi { - API_VERSION = "v3"; - - /** - * Do a GET request on the api. - * @param endpoint - the path to call - * @param query - query string as object - */ - - public async get( - endpoint: string, - query: { [key: string]: string } = {}, - ): Promise { - const url = new URL("https://www.googleapis.com"); - url.pathname = "/youtube/" + this.API_VERSION + "/" + endpoint; - url.search = new URLSearchParams(query).toString(); - - const accessToken = Storage.get("auth", "YOUTUBE_ACCESS_TOKEN"); - - Logger.trace("GET", url.href); - return await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Connection: "Keep-Alive", - Authorization: "Bearer " + accessToken, - "User-Agent": Storage.get("settings", "USER_AGENT"), - }, - }) - .then((res) => handleJsonResponse(res, true)) - .catch((err) => this.handleYouTubeError(err)) - .catch((err) => handleApiError(err)); - } - - /** - * Do a json POST request on the api. - * @param endpoint - the path to call - * @param body - body as object - - - public async postJson( - endpoint: string, - body = {}, - expectEmptyResponse = false, - ): Promise { - const url = new URL("https://api.linkedin.com"); - - const [pathname, search] = endpoint.split("?"); - url.pathname = "rest/" + pathname; - if (search) { - url.search = search; - } - const accessToken = Storage.get("auth", "LINKEDIN_ACCESS_TOKEN"); - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "Linkedin-Version": this.API_VERSION, - Authorization: "Bearer " + accessToken, - }, - body: JSON.stringify(body), - }) - .then((res) => - expectEmptyResponse - ? handleEmptyResponse(res, true) - : handleJsonResponse(res, true), - ) - .then((res) => { - const linkedinRes = res as { - id?: string; - headers?: { - "x-restli-id"?: string; - "x-linkedin-id"?: string; - }; - }; - if (!linkedinRes["id"] && "headers" in linkedinRes) { - if (linkedinRes.headers?.["x-restli-id"]) { - linkedinRes["id"] = linkedinRes.headers["x-restli-id"]; - } else if (linkedinRes.headers?.["x-linkedin-id"]) { - linkedinRes["id"] = linkedinRes.headers["x-linkedin-id"]; - } - } - return linkedinRes; - }) - .catch((err) => this.handleLinkedInError(err)) - .catch((err) => handleApiError(err)); - } - */ - - /** - * Handle api error - * - * Improve error message and rethrow it. - * @param error - ApiResponseError - */ - public async handleYouTubeError(error: ApiResponseError): Promise { - if (error.responseData) { - // - } - if (error.response?.headers) { - // - } - - throw error; - } -}