Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

Add a discord rpc service #258

Merged
merged 1 commit into from
Oct 12, 2021
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
95 changes: 95 additions & 0 deletions nodecg-io-discord-rpc/extension/discordRpcAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Required because discord-rpc does not expose the access token
// and does not even use the refresh token.
// Without this, users would be prompted on whether they agree, whenever
// the service connects to discord.

import * as rpc from "discord-rpc";
import fetch from "node-fetch";
import { URLSearchParams } from "url";

// Defined here because nodejs does not seem to accept
// circular imports.
export interface DiscordRpcConfig {
clientId: string;
clientSecret: string;
accessToken?: string;
refreshToken?: string;
redirectUrl?: string;
expireTime?: number;
}

// Fill the config values and create matching login data
export async function createLoginData(
client: rpc.Client,
config: DiscordRpcConfig,
scopes: string[],
): Promise<rpc.RPCLoginOptions> {
await client.connect(config.clientId);
const redirectUrl = config.redirectUrl === undefined ? "http://127.0.0.1" : config.redirectUrl;
if (config.accessToken === undefined || config.refreshToken === undefined || config.expireTime === undefined) {
// Call authorize
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const authorizeResult = await client.request("AUTHORIZE", {
client_id: config.clientId,
scopes: scopes,
});
const response = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: encodeQuery({
client_id: config.clientId,
client_secret: config.clientSecret,
grant_type: "authorization_code",
code: authorizeResult.code,
redirect_uri: redirectUrl,
scope: scopes.join(" "),
}),
});
const data = await response.json();
if ("error" in data)
throw new Error("error_description" in data ? data.error_description : "Unknown discord rpc login error");
config.accessToken = data.access_token;
config.refreshToken = data.refresh_token;
config.expireTime = data.expires_in + Date.now() / 1000 - 1000;
} else if (config.expireTime < Date.now() / 1000) {
// Token expired. Request a new one using the refresh token.
const response = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: encodeQuery({
client_id: config.clientId,
client_secret: config.clientSecret,
grant_type: "refresh_token",
refresh_token: config.refreshToken,
redirect_uri: redirectUrl,
scope: scopes.join(" "),
}),
});
const data = await response.json();
if ("error" in data)
throw new Error("error_description" in data ? data.error_description : "Unknown discord rpc refresh error");
config.accessToken = data.access_token;
config.refreshToken = data.refresh_token;
config.expireTime = data.expires_in + Date.now() / 1000 - 1000;
}
return {
clientId: config.clientId,
clientSecret: config.clientSecret,
redirectUri: redirectUrl,
scopes: scopes,
accessToken: config.accessToken,
};
}

function encodeQuery(query: Record<string, unknown>): URLSearchParams {
const params = new URLSearchParams();
for (const key in query) {
params.append(key, String(query[key]));
}
return params;
}
36 changes: 36 additions & 0 deletions nodecg-io-discord-rpc/extension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NodeCG } from "nodecg-types/types/server";
import { Result, emptySuccess, success, ServiceBundle } from "nodecg-io-core";
import { DiscordRpcConfig, createLoginData } from "./discordRpcAuth";
import * as rpc from "discord-rpc";

export type DiscordRpcClient = rpc.Client;

module.exports = (nodecg: NodeCG) => {
new DiscordRpcService(nodecg, "discord-rpc", __dirname, "../schema.json").register();
};

class DiscordRpcService extends ServiceBundle<DiscordRpcConfig, DiscordRpcClient> {
async validateConfig(config: DiscordRpcConfig): Promise<Result<void>> {
const client = new rpc.Client({ transport: "ipc" });
const login = await createLoginData(client, config, ["identify", "rpc"]);
await client.login(login);
await client.destroy();
return emptySuccess();
}

async createClient(config: DiscordRpcConfig): Promise<Result<DiscordRpcClient>> {
const client = new rpc.Client({ transport: "ipc" });
const login = await createLoginData(client, config, ["identify", "rpc"]);
await client.login(login);
this.nodecg.log.info("Successfully created discord-rpc client.");
return success(client);
}

stopClient(client: DiscordRpcClient): void {
client.destroy().then((_) => this.nodecg.log.info("Successfully stopped discord-rpc client."));
}

removeHandlers(client: DiscordRpcClient): void {
client.removeAllListeners();
}
}
50 changes: 50 additions & 0 deletions nodecg-io-discord-rpc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "nodecg-io-discord-rpc",
"version": "0.2.0",
"description": "Allows to interface with a locally running discord client via RPC",
"homepage": "https://nodecg.io/RELEASE/samples/discord-rpc",
"author": {
"name": "noeppi_noeppi",
"url": "https://github.com/noeppi-noeppi"
},
"repository": {
"type": "git",
"url": "https://github.com/codeoverflow-org/nodecg-io.git",
"directory": "nodecg-io-discord-rpc"
},
"files": [
"**/*.js",
"**/*.js.map",
"**/*.d.ts",
"*.json"
],
"main": "extension/index",
"scripts": {
"build": "tsc -b",
"watch": "tsc -b -w",
"clean": "tsc -b --clean"
},
"keywords": [
"nodecg-io",
"nodecg-bundle"
],
"nodecg": {
"compatibleRange": "^1.1.1",
"bundleDependencies": {
"nodecg-io-core": "^0.2.0"
}
},
"license": "MIT",
"devDependencies": {
"@types/node": "^15.0.2",
"@types/node-fetch": "^2.5.10",
"nodecg-types": "^1.8.2",
"typescript": "^4.2.4"
},
"dependencies": {
"nodecg-io-core": "^0.2.0",
"discord-rpc": "^4.0.1",
"@types/discord-rpc": "^4.0.0",
"node-fetch": "^2.6.1"
}
}
32 changes: 32 additions & 0 deletions nodecg-io-discord-rpc/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"clientId": {
"type": "string",
"description": "The client id of the discord app."
},
"clientSecret": {
"type": "string",
"description": "The client secret of the discord app."
},
"accessToken": {
"type": "string",
"description": "The client access token."
},
"refreshToken": {
"type": "string",
"description": "The client refresh token."
},
"redirectUrl": {
"type": "string",
"description": "The client redirect URL."
},
"expireTime": {
"type": "number",
"description": "Timestamp when the access token expires."
}
},
"required": ["clientId", "clientSecret"]
}
3 changes: 3 additions & 0 deletions nodecg-io-discord-rpc/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.common.json"
}
69 changes: 69 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions samples/discord-rpc/extension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NodeCG } from "nodecg-types/types/server";
import { DiscordRpcClient } from "nodecg-io-discord-rpc";
import { requireService } from "nodecg-io-core";

module.exports = function (nodecg: NodeCG) {
nodecg.log.info("Sample bundle for DiscordRpc started.");

const discordRpc = requireService<DiscordRpcClient>(nodecg, "discord-rpc");

discordRpc?.onAvailable((client) => {
nodecg.log.info("DiscordRpc service available. Username: " + client.user.username);
});

discordRpc?.onUnavailable(() => {
nodecg.log.info("DiscordRpc service unavailable.");
});
};
24 changes: 24 additions & 0 deletions samples/discord-rpc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "discord-rpc",
"version": "0.2.0",
"private": true,
"nodecg": {
"compatibleRange": "^1.1.1",
"bundleDependencies": {
"nodecg-io-discord-rpc": "0.2.0"
}
},
"scripts": {
"build": "tsc -b",
"watch": "tsc -b -w",
"clean": "tsc -b --clean"
},
"license": "MIT",
"dependencies": {
"@types/node": "^15.0.2",
"nodecg-types": "^1.8.2",
"nodecg-io-core": "^0.2.0",
"typescript": "^4.2.4",
"nodecg-io-discord-rpc": "0.2.0"
}
}
3 changes: 3 additions & 0 deletions samples/discord-rpc/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.common.json"
}