Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.
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
10 changes: 8 additions & 2 deletions nodecg-io-core/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = (nodecg: NodeCG): NodeCGIOCore => {
persistenceManager,
).registerMessageHandlers();

registerExitHandlers(nodecg, bundleManager, instanceManager, serviceManager);
registerExitHandlers(nodecg, bundleManager, instanceManager, serviceManager, persistenceManager);

// We use a extra object instead of returning a object containing all the managers and so on, because
// any loaded bundle would be able to call any (public or private) of the managers which is not intended.
Expand Down Expand Up @@ -62,7 +62,12 @@ function onExit(
bundleManager: BundleManager,
instanceManager: InstanceManager,
serviceManager: ServiceManager,
persistenceManager: PersistenceManager,
): void {
// Save everything
// This is especially important if some services update some configs (e.g. updated tokens) and they haven't been saved yet.
persistenceManager.save();

// Unset all service instances in all bundles
const bundles = bundleManager.getBundleDependencies();
for (const bundleName in bundles) {
Expand Down Expand Up @@ -99,9 +104,10 @@ function registerExitHandlers(
bundleManager: BundleManager,
instanceManager: InstanceManager,
serviceManager: ServiceManager,
persistenceManager: PersistenceManager,
): void {
const handler = () => {
onExit(nodecg, bundleManager, instanceManager, serviceManager);
onExit(nodecg, bundleManager, instanceManager, serviceManager, persistenceManager);
};

// Normal exit
Expand Down
2 changes: 1 addition & 1 deletion nodecg-io-core/extension/persistenceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class PersistenceManager {
/**
* Encrypts and saves current state to the persistent replicant.
*/
private save() {
save(): void {
// Check if we have a password to encrypt the data with.
if (this.password === undefined) {
return;
Expand Down
2 changes: 1 addition & 1 deletion nodecg-io-spotify/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class SpotifyService extends ServiceBundle<SpotifyServiceConfig, SpotifyServiceC

// This little snippet closes the oauth window after the connection was successful
const callbackWebsite =
"<http><head><script>window.close();</script></head><body>Spotify connection successful! You may close this window now.</body></http>";
"<html><head><script>window.close();</script></head><body>Spotify connection successful! You may close this window now.</body></html>";
res.send(callbackWebsite);
});

Expand Down
47 changes: 39 additions & 8 deletions nodecg-io-youtube/extension/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { NodeCG } from "nodecg/types/server";
import { Result, emptySuccess, success, error, ServiceBundle } from "nodecg-io-core";
import { google, youtube_v3 } from "googleapis";
import type { Credentials } from "google-auth-library/build/src/auth/credentials";
import type { OAuth2Client } from "google-auth-library/build/src/auth/oauth2client";
import * as express from "express";
import opn = require("open");

interface YoutubeServiceConfig {
clientID: string;
clientSecret: string;
refreshToken?: string;
}

export type YoutubeServiceClient = youtube_v3.Youtube;
Expand All @@ -26,6 +29,36 @@ class YoutubeService extends ServiceBundle<YoutubeServiceConfig, YoutubeServiceC
clientSecret: config.clientSecret,
redirectUri: "http://localhost:9090/nodecg-io-youtube/oauth2callback",
});

if (config.refreshToken) {
this.nodecg.log.info("Re-using saved refresh token.");
auth.setCredentials({
refresh_token: config.refreshToken,
});
} else {
this.nodecg.log.info("No refresh token found. Starting auth flow to get one...");
auth.setCredentials(await this.initialAuth(auth));
if (auth.credentials.refresh_token) {
config.refreshToken = auth.credentials.refresh_token;
}
}

// Save refresh tokens so they can be used next time to get a access token again
auth.on("tokens", (tokens) => {
if (tokens.refresh_token) {
config.refreshToken = tokens.refresh_token;
}
});

const client = new youtube_v3.Youtube({ auth });
return success(client);
}

stopClient(_client: YoutubeServiceClient): void {
// Cannot stop client
}

private initialAuth(auth: OAuth2Client): Promise<Credentials> {
const authUrl = auth.generateAuthUrl({
access_type: "offline",
scope: "https://www.googleapis.com/auth/youtube",
Expand All @@ -38,12 +71,14 @@ class YoutubeService extends ServiceBundle<YoutubeServiceConfig, YoutubeServiceC
router.get("/nodecg-io-youtube/oauth2callback", async (req, res) => {
try {
const params = req.query;
res.end("<script>window.close()</script>");

const callbackWebsite =
"<html><head><script>window.close();</script></head><body>YouTube connection successful! You may close this window now.</body></html>";
res.send(callbackWebsite);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { tokens } = await auth.getToken(params.code!.toString());
auth.credentials = tokens;
const client = new youtube_v3.Youtube({ auth });
resolve(success(client));
resolve(tokens);
} catch (e) {
reject(error(e));
}
Expand All @@ -53,8 +88,4 @@ class YoutubeService extends ServiceBundle<YoutubeServiceConfig, YoutubeServiceC
opn(authUrl, { wait: false }).then((cp) => cp.unref());
});
}

stopClient(_client: YoutubeServiceClient): void {
// Cannot stop client
}
}
4 changes: 4 additions & 0 deletions nodecg-io-youtube/youtube-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"clientSecret": {
"type": "string",
"description": "The oauth client secret https://console.cloud.google.com/apis/credentials/oauthclient"
},
"refreshToken": {
"type": "string",
"description": "Token that allows the client to refresh access tokens. This is set automatically after first login, you don't need to set it."
}
},
"required": ["clientID", "clientSecret"]
Expand Down