Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/LinkedIn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 27 additions & 1 deletion docs/Youtube.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions src/platforms/YouTube/YouTube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
80 changes: 42 additions & 38 deletions src/platforms/YouTube/YouTubeAuth.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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<TokenResponse> {
private async exchangeCode(code: string): Promise<Credentials> {
this.user.trace("YouTubeAuth", "exchangeCode", code);

const clientHost = this.user.get("settings", "OAUTH_HOSTNAME");
Expand All @@ -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;
}
Expand Down