Add ngate
Some checks failed
Build and Publish Docker Image / build_and_push (push) Failing after 1m17s
Some checks failed
Build and Publish Docker Image / build_and_push (push) Failing after 1m17s
This commit is contained in:
2029
package-lock.json
generated
2029
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -17,23 +17,23 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^4.1.0",
|
||||||
"discord.js": "^14.16.1",
|
"discord.js": "^14.17.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.1",
|
||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.13.0",
|
||||||
"openai": "^4.62.0",
|
"openai": "^4.82.0",
|
||||||
"replicate": "^0.32.1"
|
"replicate": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/glob": "^8.1.0",
|
"@types/glob": "^8.1.0",
|
||||||
"@types/node": "^20.4.0",
|
"@types/node": "^22.13.0",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.19.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.4.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.3.6",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
203
src/commands/ngate.ts
Normal file
203
src/commands/ngate.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
|
||||||
|
import OpenAI from "openai";
|
||||||
|
import config from "../config";
|
||||||
|
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: config.openaiApiKey,
|
||||||
|
});
|
||||||
|
const ngateModel = "ft:gpt-4o-mini-2024-07-18:personal:ngate:AvnNWGXx";
|
||||||
|
const systemPrompt = `You are a satirical news writer for the popular news aggregator site, Hacker News.
|
||||||
|
You are known for your witty, cynical and often sarcastic take on the world of technology and startups.
|
||||||
|
You write in a style that is both informative and entertaining, often using humor to make complex topics more accessible.
|
||||||
|
You believe that Hacker News comments are often deceptive, delusional or misleading, and you enjoy pointing out the absurdities and contradictions in them.
|
||||||
|
You will write a brief news summary for a given Hacker News item based on its title and the comments.`;
|
||||||
|
|
||||||
|
// Initialise the command data.
|
||||||
|
export const data = new SlashCommandBuilder()
|
||||||
|
.setName("ngate")
|
||||||
|
.setDescription(
|
||||||
|
"Generates a satirical news summary for the provided Hacker News URL, inspired by n-gate."
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("url")
|
||||||
|
.setDescription("The Hacker News URL to generate an n-gate summary for.")
|
||||||
|
.setRequired(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Loaded ${data.name} command.`);
|
||||||
|
|
||||||
|
// Define the shape of a comment after filtering.
|
||||||
|
interface Comment {
|
||||||
|
author: string;
|
||||||
|
text: string;
|
||||||
|
children: Comment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a URL like "http://news.ycombinator.com/item?id=15634609",
|
||||||
|
* returns the id ("15634609") by splitting on "=".
|
||||||
|
*/
|
||||||
|
export function extractId(url: string): string {
|
||||||
|
const parts = url.split("=");
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the comments for a given id from the HN Algolia API.
|
||||||
|
* Returns the parsed JSON if the HTTP status is 200.
|
||||||
|
*/
|
||||||
|
export async function fetchComments(id: string): Promise<any> {
|
||||||
|
const url = `https://hn.algolia.com/api/v1/items/${id}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
// You can choose to throw an error or return undefined if the status is not 200.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple function to un-escape a few common HTML entities.
|
||||||
|
* (You could replace this with a more complete solution or a library if needed.)
|
||||||
|
*/
|
||||||
|
function unescapeHTML(html: string): string {
|
||||||
|
return html
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the comments by:
|
||||||
|
* - Keeping only the "author", "text", and "children" fields.
|
||||||
|
* - Recursively processing child comments (up to a given depth).
|
||||||
|
*
|
||||||
|
* @param comments - The list of comments to filter.
|
||||||
|
* @param depth - How deep to process child comments (default is 3).
|
||||||
|
* @returns The filtered comments.
|
||||||
|
*/
|
||||||
|
export function filterComments(comments: any[], depth: number = 3): Comment[] {
|
||||||
|
if (depth === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredComments: Comment[] = [];
|
||||||
|
for (const comment of comments) {
|
||||||
|
let text: string = comment.text;
|
||||||
|
try {
|
||||||
|
text = text ? unescapeHTML(text) : "";
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error un-escaping HTML: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredComment: Comment = {
|
||||||
|
author: comment.author,
|
||||||
|
text,
|
||||||
|
children: filterComments(comment.children, depth - 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredComments.push(filteredComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredComments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively builds a pretty-printed string of comments,
|
||||||
|
* using indentation to show nesting.
|
||||||
|
*
|
||||||
|
* @param comments - The list of comments to print.
|
||||||
|
* @param indent - The current indent level (default is 0).
|
||||||
|
* @returns A string with the pretty-printed comments.
|
||||||
|
*/
|
||||||
|
export function prettyPrintComments(
|
||||||
|
comments: Comment[],
|
||||||
|
indent: number = 0
|
||||||
|
): string {
|
||||||
|
let result = "";
|
||||||
|
for (const comment of comments) {
|
||||||
|
result += "- ".repeat(indent + 1) + `${comment.author}: ${comment.text}\n`;
|
||||||
|
result += prettyPrintComments(comment.children, indent + 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a satirical news summary for the provided Hacker News URL, inspired by n-gate.
|
||||||
|
* @param interaction The interaction that triggered the command.
|
||||||
|
* @returns A promise that resolves when the command is finished executing.
|
||||||
|
*/
|
||||||
|
export async function execute(
|
||||||
|
interaction: ChatInputCommandInteraction
|
||||||
|
): Promise<void> {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const url = interaction.options.getString("url", true).trim();
|
||||||
|
|
||||||
|
// Verify URL format based on https://news.ycombinator.com/item?id=15634609 pattern.
|
||||||
|
if (!url.startsWith("https://news.ycombinator.com/item?id=")) {
|
||||||
|
await interaction.editReply("Invalid Hacker News URL format.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = extractId(url);
|
||||||
|
|
||||||
|
// Verify that the ID is a valid number.
|
||||||
|
if (isNaN(Number(id))) {
|
||||||
|
await interaction.editReply("Invalid Hacker News ID.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await fetchComments(id);
|
||||||
|
|
||||||
|
// Verify that comments were fetched successfully.
|
||||||
|
if (!comments) {
|
||||||
|
await interaction.editReply("Failed to fetch comments.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredComments = filterComments(comments.children);
|
||||||
|
|
||||||
|
// Verify that comments were filtered successfully.
|
||||||
|
if (!filteredComments.length) {
|
||||||
|
await interaction.editReply("No comments found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedComments = prettyPrintComments(filteredComments);
|
||||||
|
|
||||||
|
// Generate a prompt based on the title and comments.
|
||||||
|
const title = comments.title;
|
||||||
|
const userPrompt = `Write a news summary for the following Hacker News item:
|
||||||
|
Title: ${title}
|
||||||
|
Comments:
|
||||||
|
${formattedComments}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a chat completion.
|
||||||
|
const result = await client.chat.completions.create({
|
||||||
|
model: ngateModel,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{ role: "user", content: userPrompt },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract the response.
|
||||||
|
const response = result.choices[0]!.message?.content;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
await interaction.editReply("No response was generated.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.editReply(response);
|
||||||
|
} catch (error: any) {
|
||||||
|
await interaction.editReply(
|
||||||
|
`An error occurred while generating the response: ${error.message || "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import { Collection, REST, Routes } from 'discord.js';
|
import { Collection, REST, Routes } from "discord.js";
|
||||||
import * as birthday from '../commands/birthday';
|
import * as birthday from "../commands/birthday";
|
||||||
import * as corrupt from '../commands/corrupt';
|
import * as corrupt from "../commands/corrupt";
|
||||||
import * as countdown from '../commands/countdown';
|
import * as countdown from "../commands/countdown";
|
||||||
import * as eyecandy from '../commands/eyecandy';
|
import * as eyecandy from "../commands/eyecandy";
|
||||||
import * as game from '../commands/game';
|
import * as game from "../commands/game";
|
||||||
import * as gerard from '../commands/gerard';
|
import * as gerard from "../commands/gerard";
|
||||||
import * as image from '../commands/image';
|
import * as image from "../commands/image";
|
||||||
import * as imdb from '../commands/imdb';
|
import * as imdb from "../commands/imdb";
|
||||||
import * as kanye from '../commands/kanye';
|
import * as kanye from "../commands/kanye";
|
||||||
import * as magicEightBall from '../commands/magic8Ball';
|
import * as magicEightBall from "../commands/magic8Ball";
|
||||||
import * as payday from '../commands/payday';
|
import * as ngate from "../commands/ngate";
|
||||||
import * as plant from '../commands/plant';
|
import * as payday from "../commands/payday";
|
||||||
import * as reminder from '../commands/reminder';
|
import * as plant from "../commands/plant";
|
||||||
import * as servertime from '../commands/servertime';
|
import * as reminder from "../commands/reminder";
|
||||||
import * as taylor from '../commands/taylor';
|
import * as servertime from "../commands/servertime";
|
||||||
import * as twentyTwenty from '../commands/twentyTwenty';
|
import * as taylor from "../commands/taylor";
|
||||||
import config from '../config';
|
import * as twentyTwenty from "../commands/twentyTwenty";
|
||||||
import { Command } from './types';
|
import config from "../config";
|
||||||
|
import { Command } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all commands as a collection.
|
* Get all commands as a collection.
|
||||||
@@ -35,6 +36,7 @@ export function getCommands(): Collection<string, Command> {
|
|||||||
commands.set(imdb.data.name, imdb);
|
commands.set(imdb.data.name, imdb);
|
||||||
commands.set(kanye.data.name, kanye);
|
commands.set(kanye.data.name, kanye);
|
||||||
commands.set(magicEightBall.data.name, magicEightBall);
|
commands.set(magicEightBall.data.name, magicEightBall);
|
||||||
|
commands.set(ngate.data.name, ngate);
|
||||||
commands.set(payday.data.name, payday);
|
commands.set(payday.data.name, payday);
|
||||||
commands.set(plant.data.name, plant);
|
commands.set(plant.data.name, plant);
|
||||||
commands.set(reminder.data.name, reminder);
|
commands.set(reminder.data.name, reminder);
|
||||||
@@ -53,7 +55,7 @@ export async function registerSlashCommands(
|
|||||||
commands: Collection<string, Command>
|
commands: Collection<string, Command>
|
||||||
) {
|
) {
|
||||||
const commandData = commands.map((command) => command.data.toJSON());
|
const commandData = commands.map((command) => command.data.toJSON());
|
||||||
const rest = new REST({ version: '10' }).setToken(config.discordApiKey);
|
const rest = new REST({ version: "10" }).setToken(config.discordApiKey);
|
||||||
await rest.put(Routes.applicationCommands(config.discordApplicationId), {
|
await rest.put(Routes.applicationCommands(config.discordApplicationId), {
|
||||||
body: commandData,
|
body: commandData,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user