feat: initial project
Some checks failed
Node.js CI / Lint and Test (push) Has been cancelled

This commit is contained in:
Naomi Carrigan 2025-02-18 15:19:25 -08:00
parent 546ea428fd
commit 46ae51bd33
Signed by: naomi
SSH Key Fingerprint: SHA256:rca1iUI2OhAM6n4FIUaFcZcicmri0jgocqKiTTAfrt8
11 changed files with 5497 additions and 0 deletions

38
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,38 @@
name: Node.js CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint and Test
steps:
- name: Checkout Source Files
uses: actions/checkout@v4
- name: Use Node.js v22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Install Dependencies
run: pnpm install
- name: Lint Source Files
run: pnpm run lint
- name: Verify Build
run: pnpm run build
- name: Run Tests
run: pnpm run test

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
prod
bin/*
!bin/.gitkeep

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"],
"sonarlint.connectedMode.project": {
"connectionId": "nhcarrigan",
"projectKey": "nhcarrigan_forms-api"
}
}

0
bin/.gitkeep Normal file
View File

14
eslint.config.js Normal file
View File

@ -0,0 +1,14 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig,
{
rules: {
"max-lines-per-function": "off",
"max-statements": "off",
"unicorn/prefer-top-level-await": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"complexity": "off",
}
}
]

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "discord-rpc",
"version": "0.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"build": "tsc",
"lint": "eslint src --max-warnings 0",
"prepkg:main": "esbuild src/index.ts --bundle --platform=node --outfile=prod/index.js",
"prepkg:installer": "esbuild src/setup.ts --bundle --platform=node --outfile=prod/setup.js",
"package": "pnpm run pkg:main && pnpm run pkg:installer",
"pkg:main": "pkg prod/index.js --target latest-linux-x64 --output ./bin/naomis-drpc",
"pkg:installer": "pkg prod/setup.js --target latest-linux-x64 --output ./bin/naomis-drpc-setup",
"test": "echo \"Error: no test specified\" && exit 0"
},
"keywords": [],
"author": "",
"license": "See License in LICENSE.md",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/discord-rpc": "4.0.8",
"@types/node": "22.13.4",
"@yao-pkg/pkg": "6.3.1",
"esbuild": "0.25.0",
"eslint": "9.20.1",
"typescript": "5.7.3"
},
"dependencies": {
"discord-rpc": "4.0.1"
}
}

5165
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

86
src/index.ts Normal file
View File

@ -0,0 +1,86 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { Client } from "discord-rpc";
import type { Config } from "./types.ts";
void (async(): Promise<void> => {
const configFile = join(homedir(), ".config", "naomis-drpc", "config.json");
const rawConfig = await readFile(configFile, "utf-8").catch(() => {
return null;
});
if (!rawConfig) {
process.stderr.write(
`Config file not found. Please run naomis-drpc-setup first, or manually create a config in ${configFile}.\n`,
);
process.exit(1);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- JSON doesn't take a generic.
const config = JSON.parse(rawConfig) as Config;
const {
appId,
details,
state,
largeImageKey,
largeImageText,
smallImageKey,
smallImageText,
buttonOneLabel,
buttonOneUrl,
buttonTwoLabel,
buttonTwoUrl,
} = config;
if (
!appId
|| !details
|| !state
|| !largeImageKey
|| !largeImageText
|| !smallImageKey
|| !smallImageText
) {
process.stderr.write(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"Config file is missing required fields. Please run naomis-drpc-setup to reconfigure.\n",
);
process.exit(1);
}
process.stdout.write("Config loaded successfully.\n");
const buttons: Array<{ label: string; url: string }> = [];
if (buttonOneLabel && buttonOneUrl) {
buttons.push({ label: buttonOneLabel, url: buttonOneUrl });
}
if (buttonTwoLabel && buttonTwoUrl) {
buttons.push({ label: buttonTwoLabel, url: buttonTwoUrl });
}
const client = new Client({ transport: "ipc" });
const startTimestamp = Date.now();
client.on("ready", () => {
void client.setActivity({
buttons,
details,
largeImageKey,
largeImageText,
smallImageKey,
smallImageText,
startTimestamp,
state,
});
});
await client.login({ clientId: appId });
})();

121
src/setup.ts Normal file
View File

@ -0,0 +1,121 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { mkdir, stat, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { createInterface } from "node:readline/promises";
import type { Config } from "./types.ts";
void (async(): Promise<void> => {
const reader = createInterface({
input: process.stdin,
output: process.stdout,
});
const configDirectory = join(homedir(), ".config");
const appconfigDirectory = join(configDirectory, "naomis-drpc");
const directoryExists = await stat(appconfigDirectory).catch(() => {
return null;
});
if (!directoryExists?.isDirectory()) {
await mkdir(appconfigDirectory, { recursive: true });
}
const configFile = join(appconfigDirectory, "config.json");
process.stdout.write(`Config path: ${configFile}\n`);
const configExists = await stat(configFile).catch(() => {
return null;
});
if (configExists?.isFile()) {
const confirmOverwrite = await reader.question(
"Config file already exists. Overwrite? (y/N) ",
);
if (confirmOverwrite.toLowerCase() !== "y") {
process.stdout.write("Declined to overwrite config file. Exiting...\n");
process.exit(0);
}
}
const config: Partial<Config> = {};
process.stdout.write("Welcome to the Discord Rich Presence Setup Wizard!\n");
process.stdout.write(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"Before you get started, you need to create a Discord application on their developer portal. https://discord.dev\n",
);
process.stdout.write(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"The name you assign your new application will show up in your Discord STATUS when you are connected with this tool.",
);
const appId = await reader.question(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"Enter your Discord application ID. You can obtain this from the Discord Developer Portal:",
);
config.appId = appId;
const details = await reader.question(
"Enter the first line of text you'd like to show in your presence:",
);
config.details = details;
const state = await reader.question(
"Enter the second line of text you'd like to show in your presence:",
);
config.state = state;
process.stdout.write(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"The next section sets up the images to display in your presence. You can upload these images to your application under Rich Presence -> Art Assets.\n",
);
const largeImageKey = await reader.question(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"Enter the DISCORD name of the image you uploaded to display as the large image:",
);
config.largeImageKey = largeImageKey;
const largeImageText = await reader.question(
"Enter the text to display when hovering over the large image:",
);
config.largeImageText = largeImageText;
const smallImageKey = await reader.question(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"Enter the DISCORD name of the image you uploaded to display as the small image:",
);
config.smallImageKey = smallImageKey;
const smallImageText = await reader.question(
"Enter the text to display when hovering over the small image:",
);
config.smallImageText = smallImageText;
process.stdout.write(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"You can optionally display buttons that other users can click on in your presence.\n",
);
const wantsButtons = await reader.question(
"Do you want to display buttons in your presence? (y/N) ",
);
if (wantsButtons.toLowerCase() === "y") {
const buttonOneLabel = await reader.question(
"Enter the text to display on the first button:",
);
config.buttonOneLabel = buttonOneLabel;
const buttonOneUrl = await reader.question(
"Enter the URL to open when the first button is clicked:",
);
config.buttonOneUrl = buttonOneUrl;
const buttonTwoLabel = await reader.question(
"Enter the text to display on the second button:",
);
config.buttonTwoLabel = buttonTwoLabel;
const buttonTwoUrl = await reader.question(
"Enter the URL to open when the second button is clicked:",
);
config.buttonTwoUrl = buttonTwoUrl;
}
await writeFile(configFile, JSON.stringify(config, null, 4));
process.stdout.write(
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"Config file written successfully! You can now run naomis-drpc to start your Discord Rich Presence.\n",
);
process.exit(0);
})();

19
src/types.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Config {
appId: string;
details: string;
state: string;
largeImageKey: string;
largeImageText: string;
smallImageKey: string;
smallImageText: string;
buttonOneLabel?: string;
buttonOneUrl?: string;
buttonTwoLabel?: string;
buttonTwoUrl?: string;
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./prod",
}
}