diff --git a/.env.dist b/.env.dist index 3a9868d..67e8aa7 100644 --- a/.env.dist +++ b/.env.dist @@ -74,3 +74,9 @@ 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 +# FAIRPOST_YOUTUBE_PRIVACY=public +# FAIRPOST_YOUTUBE_CATEGORY=test \ No newline at end of file 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/NewPlatform.md b/docs/NewPlatform.md new file mode 100644 index 0000000..9f54b9a --- /dev/null +++ b/docs/NewPlatform.md @@ -0,0 +1,290 @@ +# 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 `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: + +```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) { + const redirectUri = OAuth2Service.getCallbackUrl(); + // implement your own post method ... + 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) { + Storage.set("auth", "FOOBAR_ACCESS_TOKEN", tokens["access_token"]); + } + +} + +``` \ No newline at end of file diff --git a/docs/Youtube.md b/docs/Youtube.md new file mode 100644 index 0000000..0cb27a6 --- /dev/null +++ b/docs/Youtube.md @@ -0,0 +1,105 @@ +# Platform: YouTube + +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 + +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 + - 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 + - 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` + +### 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 + +- `FAIRPOST_YOUTUBE_PRIVACY` = public | private +- `FAIRPOST_YOUTUBE_CATEGORY` = valid youtube category id + +## Manage additional pages with the same app + +... + +# Limitations + +## Video + +### Supported Formats +Accepted Media MIME types: +video/*, application/octet-stream + +### File Size +Maximum file size: 256GB + + +# Random documentation + +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 + +scopes +https://www.googleapis.com/auth/youtube.force-ssl +https://www.googleapis.com/auth/youtube.readonly +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 new file mode 100644 index 0000000..9b44c14 --- /dev/null +++ b/src/platforms/YouTube/YouTube.ts @@ -0,0 +1,206 @@ +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 { + id: PlatformId = PlatformId.YOUTUBE; + assetsFolder = "_youtube"; + postFileName = "post.json"; + + 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(); + } + + /** @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; + }; + 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, + "https://www.youtube.com/watch?v=" + response.id, + { + 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 client = this.auth.getClient(); + const result = (await client.channels.list({ + part: ["snippet", "contentDetails", "status"], + mine: true, + })) as { + data?: { + items?: { + id: string; + snippet: { + title: string; + customUrl: string; + }; + }[]; + }; + status: number; + statusText: string; + }; + if (result.data?.items?.length) { + return { + id: result.data.items[0].id, + snippet: { + title: result.data.items[0].snippet.title, + customUrl: result.data.items[0].snippet.customUrl, + }, + }; + } + throw Logger.error("YouTube.getChannel", "invalid result", result); + } + + /** + * 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); + + 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 }; + } +} diff --git a/src/platforms/YouTube/YouTubeAuth.ts b/src/platforms/YouTube/YouTubeAuth.ts new file mode 100644 index 0000000..e752207 --- /dev/null +++ b/src/platforms/YouTube/YouTubeAuth.ts @@ -0,0 +1,157 @@ +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 { + client?: youtube_v3.Youtube; + + /** + * Set up YouTube platform + */ + async setup() { + const code = await this.requestCode(); + const tokens = await this.exchangeCode(code); + this.store(tokens); + } + + /** + * Refresh YouTube tokens + */ + async refresh() { + 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"), + }); + 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); + } + + /** + * Get or create a YouTube client + * @returns - youtube_v3.Youtube + */ + public getClient(): youtube_v3.Youtube { + if (this.client) { + return this.client; + } + const auth = new OAuth2Client(); + auth.setCredentials({ + access_token: Storage.get("auth", "YOUTUBE_ACCESS_TOKEN"), + }); + this.client = new youtube_v3.Youtube({ auth }); + return this.client; + } + + /** + * Request remote code using OAuth2Service + * @returns - code + */ + private async requestCode(): Promise { + Logger.trace("YouTubeAuth", "requestCode"); + const state = String(Math.random()).substring(2); + + 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", + ], + state: state, + }); + + const result = await OAuth2Service.requestRemotePermissions("YouTube", url); + 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 auth = new OAuth2Client( + Storage.get("settings", "YOUTUBE_CLIENT_ID"), + Storage.get("settings", "YOUTUBE_CLIENT_SECRET"), + OAuth2Service.getCallbackUrl(), + ); + + const response = (await auth.getToken(code)) as { + tokens: TokenResponse; + }; + if (!isTokenResponse(response.tokens)) { + throw Logger.error("Invalid TokenResponse", response.tokens); + } + return response.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(tokens["expiry_date"]).toISOString(); + Storage.set("auth", "YOUTUBE_ACCESS_EXPIRY", accessExpiry); + if ("scope" in tokens) { + Storage.set("auth", "YOUTUBE_SCOPE", tokens["scope"] ?? ""); + } + if ("refresh_token" in tokens) { + Storage.set( + "auth", + "YOUTUBE_REFRESH_TOKEN", + tokens["refresh_token"] ?? "", + ); + } + } +} + +interface TokenResponse { + access_token: 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("expiry_date" 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",