diff --git a/docs/LinkedIn.md b/docs/LinkedIn.md index 0330fd9..cfa0055 100644 --- a/docs/LinkedIn.md +++ b/docs/LinkedIn.md @@ -49,6 +49,7 @@ To get this working, you need to follow instruction at [Set up for multiple user ## Add a second user - call `./fairpost.js add-user --user=foo` # todo +- add linkedin to its FAIRPOST_PLATFORMS - find your company id (in the url, like , 93841222) - save this as `FAIRPOST_LINKEDIN_COMPANY_ID` in your users .env diff --git a/docs/Youtube.md b/docs/Youtube.md index 10afe8f..5739a3d 100644 --- a/docs/Youtube.md +++ b/docs/Youtube.md @@ -68,9 +68,19 @@ To have Fairpost publish **public** videos, your app has to be audited ## Manage additional pages with the same app -... To get this working, you need to follow instruction at [Set up for multiple users](./docs/MultipleUsers.md) +## Add a second user +- call `./fairpost.js add-user --user=foo` # todo +- add youtube to its FAIRPOST_PLATFORMS + +### Get an OAuth2 Access Token for your other page + + - call `./fairpost.js @foo setup-platform --platform=youtube` + - follow instructions from the command line + +### Test the other installation + - call `./fairpost.js @foo test-platform --platform=youtube` # Limitations @@ -108,3 +118,19 @@ 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 + + +https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest/google-auth-library/oauth2client + +refreshAccessToken(callback) +refreshToken(refreshToken) +refreshTokenNoCache(refreshToken) +getAccessToken() +isTokenExpiring() + + +https://googleapis.dev/nodejs/google-auth-library/9.8.0/#oauth2 + +https://google-auth.readthedocs.io/en/stable/reference/google.oauth2.credentials.html +https://googleapis.dev/nodejs/google-auth-library/8.5.2/interfaces/Credentials.html +https://googleapis.dev/nodejs/google-auth-library/8.5.2/interfaces/GetAccessTokenResponse.html \ No newline at end of file diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 4327091..fcca32f 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -98,6 +98,7 @@ export default class YouTube extends Platform { * @returns object, incl. some ids and names */ private async getChannel() { + this.user.trace("YouTube", "getChannel"); const client = this.auth.getClient(); const result = (await client.channels.list({ part: ["snippet", "contentDetails", "status"], diff --git a/src/platforms/YouTube/YouTubeAuth.ts b/src/platforms/YouTube/YouTubeAuth.ts index a354694..636da5f 100644 --- a/src/platforms/YouTube/YouTubeAuth.ts +++ b/src/platforms/YouTube/YouTubeAuth.ts @@ -1,4 +1,5 @@ -import { OAuth2Client } from "google-auth-library"; +import { Credentials, OAuth2Client } from "google-auth-library"; + import OAuth2Service from "../../services/OAuth2Service"; import User from "../../models/User"; import { strict as assert } from "assert"; @@ -26,23 +27,29 @@ export default class YouTubeAuth { * Refresh YouTube tokens */ async refresh() { + this.user.trace("YouTubeAuth", "refresh"); const auth = new OAuth2Client( this.user.get("settings", "YOUTUBE_CLIENT_ID"), this.user.get("settings", "YOUTUBE_CLIENT_SECRET"), ); auth.setCredentials({ + access_token: this.user.get("auth", "YOUTUBE_ACCESS_TOKEN"), refresh_token: this.user.get("auth", "YOUTUBE_REFRESH_TOKEN"), }); - const response = (await auth.getAccessToken()) as { - res: { data: TokenResponse }; + const response = (await auth.refreshAccessToken()) as { + res?: { data: Credentials }; + credentials?: Credentials; }; - if (isTokenResponse(response["res"]["data"])) { + if (response["res"]?.["data"] && isCredentials(response["res"]["data"])) { this.store(response["res"]["data"]); return; + } else if (response.credentials) { + this.store(response.credentials); + return; } throw this.user.error( "YouTubeAuth.refresh", - "not a valid repsonse", + "not a valid response", response, ); } @@ -55,9 +62,17 @@ export default class YouTubeAuth { if (this.client) { return this.client; } - const auth = new OAuth2Client(); + const auth = new OAuth2Client( + this.user.get("settings", "YOUTUBE_CLIENT_ID"), + this.user.get("settings", "YOUTUBE_CLIENT_SECRET"), + ); auth.setCredentials({ access_token: this.user.get("auth", "YOUTUBE_ACCESS_TOKEN"), + refresh_token: this.user.get("auth", "YOUTUBE_REFRESH_TOKEN"), + }); + auth.on("tokens", (creds) => { + this.user.trace("YouTubeAuth", "tokens event received"); + this.store(creds); }); this.client = new youtube_v3.Youtube({ auth }); return this.client; @@ -112,9 +127,9 @@ export default class YouTubeAuth { /** * Exchange remote code for tokens * @param code - the code to exchange - * @returns - TokenResponse + * @returns - Credentials */ - private async exchangeCode(code: string): Promise { + private async exchangeCode(code: string): Promise { this.user.trace("YouTubeAuth", "exchangeCode", code); const clientHost = this.user.get("settings", "OAUTH_HOSTNAME"); @@ -126,49 +141,38 @@ export default class YouTubeAuth { OAuth2Service.getCallbackUrl(clientHost, clientPort), ); - const response = (await auth.getToken(code)) as { - tokens: TokenResponse; - }; - if (!isTokenResponse(response.tokens)) { - throw this.user.error("Invalid TokenResponse", response.tokens); + const response = await auth.getToken(code); + if (!isCredentials(response.tokens)) { + throw this.user.error("Invalid response for getToken", response); } return response.tokens; } /** * Save all tokens in auth store - * @param tokens - the tokens to store + * @param creds - contains the tokens to store */ - private store(tokens: TokenResponse) { - this.user.set("auth", "YOUTUBE_ACCESS_TOKEN", tokens["access_token"]); - const accessExpiry = new Date(tokens["expiry_date"]).toISOString(); - this.user.set("auth", "YOUTUBE_ACCESS_EXPIRY", accessExpiry); - if ("scope" in tokens) { - this.user.set("auth", "YOUTUBE_SCOPE", tokens["scope"] ?? ""); + private store(creds: Credentials) { + this.user.trace("YouTubeAuth", "store"); + if (creds.access_token) { + this.user.set("auth", "YOUTUBE_ACCESS_TOKEN", creds.access_token); + } + if (creds.expiry_date) { + const accessExpiry = new Date(creds.expiry_date).toISOString(); + this.user.set("auth", "YOUTUBE_ACCESS_EXPIRY", accessExpiry); } - if ("refresh_token" in tokens) { - this.user.set( - "auth", - "YOUTUBE_REFRESH_TOKEN", - tokens["refresh_token"] ?? "", - ); + if (creds.scope) { + this.user.set("auth", "YOUTUBE_SCOPE", creds.scope); + } + if (creds.refresh_token) { + this.user.set("auth", "YOUTUBE_REFRESH_TOKEN", creds.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) { +function isCredentials(creds: Credentials) { try { - assert("access_token" in tokens); - assert("expiry_date" in tokens); + assert("access_token" in creds || "refresh_token" in creds); } catch (e) { return false; }