Compare commits

..

No commits in common. "main" and "feature/quota" have entirely different histories.

19 changed files with 737 additions and 1339 deletions

View file

@ -12,20 +12,7 @@ RUN npx prisma generate
# Typescript compiling # Typescript compiling
COPY tsconfig.json . COPY tsconfig.json .
COPY src ./src COPY src ./src
COPY scripts ./scripts
RUN npx tsc RUN npx tsc
# Permissions for dist directory,
# so config.ts can be compiled there during runtime
# regardless of the user of the container
RUN chmod 777 dist
# Create a db directory for sqlite database file
# it is required because in order to write to sqlite file
# the directory must be writable
RUN mkdir /db
RUN chmod 777 /db
# Run the app # Run the app
CMD ["node", "dist/src/index.js"] CMD ["node", "dist/index.js"]
STOPSIGNAL SIGINT

1118
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,30 +2,29 @@
"name": "gptcord", "name": "gptcord",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"main": "./dist/src/index.js", "main": "./dist/index.js",
"scripts": { "scripts": {
"start": "tsc && node dist/src/index.js", "start": "tsc && node dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"publishCommands": "tsc && node dist/scripts/pushCommands.js" "publishCommands": "tsc && node dist/scripts/pushCommands.js"
}, },
"author": "Wroclaw", "author": "Wroclaw",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@prisma/client": "5.13.0", "@prisma/client": "^5.0.0",
"discord.js": "14.14.1", "discord.js": "^14.8.0",
"fold-to-ascii": "^5.0.1", "fold-to-ascii": "^5.0.1",
"gpt-3-encoder": "^1.1.4", "gpt-3-encoder": "^1.1.4",
"openai": "^4.38.3", "openai": "^3.2.1",
"require-directory": "^2.1.1", "require-directory": "^2.1.1"
"typescript": "^5.4.5"
}, },
"devDependencies": { "devDependencies": {
"@types/fold-to-ascii": "^5.0.0", "@types/fold-to-ascii": "^5.0.0",
"@types/require-directory": "^2.1.2", "@types/require-directory": "^2.1.2",
"@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^7.7.1", "@typescript-eslint/parser": "^6.2.0",
"eslint": "^8.57.0", "eslint": "^8.46.0",
"json-schema-to-ts": "^3.0.1", "prisma": "^5.0.0",
"prisma": "5.13.0" "typescript": "^5.1.6"
} }
} }

View file

@ -1,66 +0,0 @@
// https://discordjs.guide/creating-your-bot/command-deployment.html#guild-commands
import { REST, RESTGetAPIOAuth2CurrentApplicationResult, RESTPostAPIApplicationCommandsJSONBody, Routes } from "discord.js";
import { config } from "../src/index";
import requireDirectory from "require-directory";
import
Command
, {
ApplicationIntegrationType
, InteractionContextTypes
, InteractionTypeMap
} from "../src/command";
const post: RESTPostAPIApplicationCommandsJSONBody[] = [];
const guildId = process.argv.slice(2)[0];
const importedCommands = requireDirectory(module, "../src/commands");
function isGuildCommand(command: Command<keyof InteractionTypeMap>): boolean {
// guild Commmand is when it's a guild install and context is guild (and these are defaults if not provided)
return (command.integration_types?.includes(ApplicationIntegrationType.Guild_Install) ?? true)
&& (command.contexts?.includes(InteractionContextTypes.Guild) ?? true);
}
for (const obj in importedCommands) {
try {
const allExports = importedCommands[obj] as {default: unknown};
const defaultExport = allExports.default;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const constructedExport = new defaultExport() as unknown;
if (!(constructedExport instanceof Command)) throw new Error(`${obj}'s default does not extends Command`);
if (guildId && guildId !== "" && isGuildCommand(constructedExport as Command<keyof InteractionTypeMap>)) {
console.log(`Skipping ${obj} because it's not a guild command`);
continue;
}
post.push(constructedExport.toRESTPostApplicationCommands());
} catch (e) {
console.error(e);
}
}
const rest = new REST().setToken(config.tokens.Discord);
(async () => {
const me = await rest.get(Routes.oauth2CurrentApplication()) as RESTGetAPIOAuth2CurrentApplicationResult;
if (guildId && guildId !== "") {
console.log(`Started refreshing ${post.length} application guild (${guildId}) commands.`);
await rest.put(
Routes.applicationGuildCommands(me.id, guildId),
{ body: post },
);
} else {
console.log(`Started refreshing ${post.length} application global commands.`);
await rest.put(
Routes.applicationCommands(me.id),
{ body: post },
);
}
console.log("Refreshed successfully");
process.exit(0);
})().catch( e => {
console.error(e);
process.exit(1);
});

View file

@ -1,78 +0,0 @@
{ pkgs ? import <nixpkgs> {}
, unstable ? import <nixos-unstable> {}
}:
let
prisma-version = "5.13.0";
prisma-src = pkgs.fetchFromGitHub {
owner = "prisma";
repo = "prisma-engines";
rev = prisma-version;
hash = "sha256-8LC2RV3FRr1F0TZxQNxvvEoTyhKusgzB5omlxLAnHG0=";
};
new-prisma-engines = unstable.rustPlatform.buildRustPackage {
pname = "prisma-engines";
version = prisma-version;
src = builtins.storePath prisma-src;
# Use system openssl.
OPENSSL_NO_VENDOR = 1;
nativeBuildInputs = [ pkgs.pkg-config pkgs.git ];
buildInputs = [
pkgs.openssl
pkgs.protobuf
];
cargoLock = {
lockFile = "${prisma-src}/Cargo.lock";
outputHashes = {
"cuid-1.3.2" = "sha256-qBu1k/dJiA6rWBwk4nOOqouIneD9h2TTBT8tvs0TDfA=";
"barrel-0.6.6-alpha.0" = "sha256-USh0lQ1z+3Spgc69bRFySUzhuY79qprLlEExTmYWFN8=";
"graphql-parser-0.3.0" = "sha256-0ZAsj2mW6fCLhwTETucjbu4rPNzfbNiHu2wVTBlTNe4=";
"mysql_async-0.31.3" = "sha256-2wOupQ/LFV9pUifqBLwTvA0tySv+XWbxHiqs7iTzvvg=";
"postgres-native-tls-0.5.0" = "sha256-UYPsxhCkXXWk8yPbqjNS0illwjS5mVm3Z/jFwpVwqfw=";
};
};
preBuild = ''
export OPENSSL_DIR=${pkgs.lib.getDev pkgs.openssl}
export OPENSSL_LIB_DIR=${pkgs.lib.getLib pkgs.openssl}/lib
export PROTOC=${pkgs.protobuf}/bin/protoc
export PROTOC_INCLUDE="${pkgs.protobuf}/include";
export SQLITE_MAX_VARIABLE_NUMBER=250000
export SQLITE_MAX_EXPR_DEPTH=10000
'';
cargoBuildFlags = [
"-p" "query-engine"
"-p" "query-engine-node-api"
"-p" "schema-engine-cli"
"-p" "prisma-fmt"
];
postInstall = ''
mv $out/lib/libquery_engine${pkgs.stdenv.hostPlatform.extensions.sharedLibrary} $out/lib/libquery_engine.node
'';
# Tests are long to compile
doCheck = false;
};
in
pkgs.mkShell {
nativeBuildInputs = [
new-prisma-engines
pkgs.nodejs_18
pkgs.openssl
];
shellHook = ''
export PRISMA_SCHEMA_ENGINE_BINARY="${new-prisma-engines}/bin/schema-engine"
export PRISMA_QUERY_ENGINE_BINARY="${new-prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${new-prisma-engines}/lib/libquery_engine.node"
export PRISMA_FMT_BINARY="${new-prisma-engines}/bin/prisma-fmt"
'';
}

View file

@ -1,61 +1,23 @@
import { AutocompleteInteraction, PermissionsBitField } from "discord.js"; import { AutocompleteInteraction, PermissionsBitField } from "discord.js";
import { RESTPostAPIApplicationCommandsJSONBody } from "discord.js"; import { RESTPostAPIApplicationCommandsJSONBody } from "discord.js";
import { APIApplicationCommandOption, ApplicationCommandType, ChatInputCommandInteraction, LocalizationMap, MessageInteraction, PermissionResolvable, UserSelectMenuInteraction } from "discord.js"; import { ApplicationCommandOption, ApplicationCommandType, ChatInputCommandInteraction, LocalizationMap, MessageInteraction, PermissionResolvable, UserSelectMenuInteraction } from "discord.js";
export type InteractionTypeMap = { type InteractionTypeMap = {
// [CommandType]: [Interaction, Description]
[ApplicationCommandType.ChatInput]: [ChatInputCommandInteraction, string]; [ApplicationCommandType.ChatInput]: [ChatInputCommandInteraction, string];
[ApplicationCommandType.Message]: [MessageInteraction, never]; [ApplicationCommandType.Message]: [MessageInteraction, never];
[ApplicationCommandType.User]: [UserSelectMenuInteraction, never]; [ApplicationCommandType.User]: [UserSelectMenuInteraction, never];
}; };
// TODO: At time of coding, Discord api types doesn't support user installations of bot/application yet
// replace this with the types from the discord api types when it's available
/**
* https://discord.com/developers/docs/resources/application#application-object-application-integration-types
*/
export enum ApplicationIntegrationType {
Guild_Install = 0,
User_Install = 1,
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types
*/
export enum InteractionContextTypes {
Guild = 0,
BotDM = 1,
PrivateChannel = 2,
}
/**
* https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
*/
export type FutureRESTPostAPIApplicationCommandsJSONBody =
RESTPostAPIApplicationCommandsJSONBody
& {
/**
* @deprecated use contexts instead
*/
dm_permission?: boolean;
integration_types?: ApplicationIntegrationType[];
contexts?: InteractionContextTypes[];
};
interface Command<Type extends keyof InteractionTypeMap = ApplicationCommandType> { interface Command<Type extends keyof InteractionTypeMap = ApplicationCommandType> {
readonly name: string; readonly name: string;
readonly name_localizations?: LocalizationMap; readonly name_localizations?: LocalizationMap;
readonly description: InteractionTypeMap[Type][1]; readonly description: InteractionTypeMap[Type][1];
readonly description_localizations?: LocalizationMap; readonly description_localizations?: LocalizationMap;
readonly options?: APIApplicationCommandOption[]; readonly options?: ApplicationCommandOption[];
readonly default_member_permissions?: PermissionResolvable; readonly default_member_permissions?: PermissionResolvable;
readonly type: Type; readonly type: Type;
readonly nsfw?: boolean; readonly nsfw?: boolean;
/** @deprecated use contexts instead */
readonly dm_permission?: boolean; readonly dm_permission?: boolean;
readonly integration_types?: ApplicationIntegrationType[];
readonly contexts?: InteractionContextTypes[];
} }
abstract class Command<Type extends keyof InteractionTypeMap = ApplicationCommandType> { abstract class Command<Type extends keyof InteractionTypeMap = ApplicationCommandType> {
@ -63,7 +25,7 @@ abstract class Command<Type extends keyof InteractionTypeMap = ApplicationComman
autocomplete?(interaction: Type extends ApplicationCommandType.ChatInput ? AutocompleteInteraction : never ): Promise<void>; autocomplete?(interaction: Type extends ApplicationCommandType.ChatInput ? AutocompleteInteraction : never ): Promise<void>;
toRESTPostApplicationCommands(): FutureRESTPostAPIApplicationCommandsJSONBody { toRESTPostApplicationCommands(): RESTPostAPIApplicationCommandsJSONBody {
return { return {
name: this.name, name: this.name,
name_localizations: this.name_localizations, name_localizations: this.name_localizations,
@ -74,8 +36,6 @@ abstract class Command<Type extends keyof InteractionTypeMap = ApplicationComman
type: this.type, type: this.type,
nsfw: this.nsfw, nsfw: this.nsfw,
dm_permission: this.dm_permission, dm_permission: this.dm_permission,
integration_types: this.integration_types,
contexts: this.contexts,
}; };
} }
} }

View file

@ -1,90 +0,0 @@
import {
APIApplicationCommandOption
, ApplicationCommandOptionType
, ApplicationCommandType
, ChatInputCommandInteraction
} from "discord.js";
import { ChatCompletionMessageParam } from "openai/resources";
import
Command
,{ApplicationIntegrationType
, InteractionContextTypes
} from "../command";
import { config } from "../index";
import { executeChatCompletion, replyInMultiMessage } from "../execution";
import { formatName } from "../toOpenAIMessages";
export default class Ask extends Command implements Command {
name = "ask";
description = "Promts the bot to reply to a single message without any history context";
type = ApplicationCommandType.ChatInput;
options: APIApplicationCommandOption[] = [
{
name: "content",
description: "The content of the prompt",
type: ApplicationCommandOptionType.String,
required: true,
},
{
name: "ephemeral",
description: "if true, only you can see the response (default true)",
type: ApplicationCommandOptionType.Boolean,
required: false,
}
];
integration_types = [
ApplicationIntegrationType.Guild_Install,
ApplicationIntegrationType.User_Install,
];
contexts = [
InteractionContextTypes.Guild,
InteractionContextTypes.BotDM,
InteractionContextTypes.PrivateChannel,
];
async execute(interaction: ChatInputCommandInteraction) {
const content = interaction.options.getString("content", true);
const ephemeral = interaction.options.getBoolean("ephemeral", false) ?? true;
if (!interaction.channel && !interaction.channelId) {
console.error("No channel found in interaction");
console.error(interaction);
await interaction.reply({
content: "No channel found in interaction???",
ephemeral: true
});
return;
}
const userLimit = await config.quota.checkUser(interaction.user, interaction);
if (userLimit.used >= userLimit.quota) {
interaction.reply({
embeds: [{
color: 0xff0000,
description: "You've used up your quota,\n" + userLimit.toString(),
}],
ephemeral: true,
}).catch(e => {
console.error("Failed to reply to user: ", e);
});
return;
}
// TODO: check content in moderation API
const messages: ChatCompletionMessageParam[] = [
...config.systemPrompt(interaction),
{ role: "user", name: formatName(interaction.user.displayName), content }
];
const [answer] = await Promise.all([
executeChatCompletion(messages, interaction),
interaction.deferReply({ ephemeral }),
]);
await replyInMultiMessage(answer.choices[0].message.content, interaction);
}
}

View file

@ -1,20 +1,16 @@
import { ApplicationCommandType, ChatInputCommandInteraction, APIApplicationCommandOption, ApplicationCommandOptionType, APIEmbedField } from "discord.js"; import { ApplicationCommandType, ChatInputCommandInteraction, ApplicationCommandOption, ApplicationCommandOptionType, APIEmbedField } from "discord.js";
import import Command from "../command";
Command
,{ApplicationIntegrationType
, InteractionContextTypes
} from "../command";
import { config } from "../index"; import { config } from "../index";
export default class MyLimit extends Command implements Command { export default class MyLimit extends Command implements Command {
name = "check-quota"; name = "check-limit";
description = "Checks your quota and usage"; description = "Checks your limit and usage";
type = ApplicationCommandType.ChatInput; type = ApplicationCommandType.ChatInput;
options: APIApplicationCommandOption[] = [ options: ApplicationCommandOption[] = [
{ {
name: "recovery-for", name: "recovery-for",
description: "Get the recovery time for given quota units count (default: until can use the bot or 1)", description: "Calculate the limit recovery time for given message count (default 1)",
type: ApplicationCommandOptionType.Integer, type: ApplicationCommandOptionType.Integer,
required: false, required: false,
}, },
@ -22,34 +18,13 @@ export default class MyLimit extends Command implements Command {
name: "ephemeral", name: "ephemeral",
description: "if true, only you can see the response (default true)", description: "if true, only you can see the response (default true)",
type: ApplicationCommandOptionType.Boolean, type: ApplicationCommandOptionType.Boolean,
required: false,
} }
]; ];
integration_types = [
ApplicationIntegrationType.Guild_Install,
ApplicationIntegrationType.User_Install
];
contexts = [
InteractionContextTypes.Guild,
InteractionContextTypes.BotDM,
InteractionContextTypes.PrivateChannel
];
async execute(interaction: ChatInputCommandInteraction) { async execute(interaction: ChatInputCommandInteraction) {
let recoveryFor = interaction.options.getInteger("recovery-for", false); let recoveryFor = interaction.options.getInteger("recovery-for", false) ?? 1;
const ephemeral = interaction.options.getBoolean("ephemeral", false) ?? true; const ephemeral = interaction.options.getBoolean("ephemeral", false) ?? true;
if (recoveryFor === null) {
const userQuota = await config.quota.checkUser(
interaction.user,
interaction,
);
const remaining = userQuota.quota - userQuota.used;
// NOTE: In order for user to execute the bot again, the used must be lower than quota
// user can't execute if quota and used are equal
// thus why we add 1 here
recoveryFor = remaining >= 0 ? 1 : 1 - remaining;
}
if (recoveryFor <= 0) recoveryFor = 1; if (recoveryFor <= 0) recoveryFor = 1;
const userQuotaRecovery = await config.quota.getUserQuotaRecovery(interaction.user, interaction, recoveryFor); const userQuotaRecovery = await config.quota.getUserQuotaRecovery(interaction.user, interaction, recoveryFor);

View file

@ -1,26 +0,0 @@
import {ApplicationCommandType, ChatInputCommandInteraction } from "discord.js";
import Command, { ApplicationIntegrationType, InteractionContextTypes } from "../command";
export default class Listen extends Command implements Command {
// This command exists because Discord bots don't receive direct messages
// unless they explicitly open a DM channel with the user
name = "listen";
description = "Makes the bot listen on your direct messages";
type = ApplicationCommandType.ChatInput;
options = [];
integration_types = [
ApplicationIntegrationType.Guild_Install,
ApplicationIntegrationType.User_Install
];
contexts = [InteractionContextTypes.BotDM];
async execute(interaction: ChatInputCommandInteraction) {
await interaction.user.createDM();
await interaction.reply({
content: "I'm now listening to your direct messages",
ephemeral: true,
});
}
}

View file

@ -1,29 +1,23 @@
import { Message } from "discord.js";
import { import {
ActivityType ChatCompletionRequestMessage as OpenAIMessage,
, type PresenceStatusData CreateChatCompletionRequest as ChatCompletionRequestData,
, type PresenceData } from "openai";
} from "discord.js";
import {
ChatCompletionMessageParam as OpenAIMessage,
ChatCompletionCreateParamsNonStreaming as ChatCompletionRequestData,
} from "openai/resources/chat";
import IQuota from "./IQuota"; import IQuota from "./IQuota";
import MessageCount from "./quota/messageCount"; import MessageCount from "./quota/messageCount";
import { apiRequest } from "./execution";
export interface IConfigRequired { export interface IConfigRequired {
readonly calendarParams: Intl.DateTimeFormatOptions;
/** Tokens to authentiate with */ /** Tokens to authentiate with */
readonly tokens: { readonly tokens: {
readonly Discord: string; readonly Discord: string;
readonly OpenAI: string; readonly OpenAI: string;
}; };
/** Discord bot status */
readonly status: PresenceData
/** Messages to append at the start of every chat history when sending to API */ /** Messages to append at the start of every chat history when sending to API */
systemPrompt(context: apiRequest): OpenAIMessage[]; systemPrompt(context: Message): OpenAIMessage[];
/** OpenAI model config */ /** OpenAI model config */
readonly chatCompletionParams: Omit<ChatCompletionRequestData, "messages" | "function_call" | "tool_call" | "functions" | "n">; readonly chatCompletionParams: Omit<ChatCompletionRequestData, "messages" | "function_call" | "functions" | "n">;
/** Limits for message selection */ /** Limits for message selection */
readonly readLimits: { readonly readLimits: {
/** Maximum message age to include (in miliseconds) */ /** Maximum message age to include (in miliseconds) */
@ -46,10 +40,6 @@ export default function newConfig(config?: IConfig): IConfigRequired {
return { ...defaultConfig, ...config }; return { ...defaultConfig, ...config };
} }
function isEnvDefined(key: string): boolean {
return process.env[key] !== undefined;
}
function envAsString(key: string): string | undefined { function envAsString(key: string): string | undefined {
key = key.toLocaleUpperCase(); key = key.toLocaleUpperCase();
return process.env[key]; return process.env[key];
@ -61,54 +51,16 @@ function envAsNumber(key: string): number | undefined {
return !Number.isNaN(value) ? value : undefined; return !Number.isNaN(value) ? value : undefined;
} }
function envAsBoolean(key: string): boolean | undefined {
key = key.toUpperCase();
const value = process.env[key];
return !(value === "false" || value === "0");
}
function envAsActivityType(key: string): ActivityType | undefined {
key = key.toUpperCase();
const value = process.env[key]?.toUpperCase();
switch (value) {
case "0":
case "PLAYING":
return ActivityType.Playing;
case "1":
case "STREAMING":
return ActivityType.Streaming;
case "2":
case "LISTENING":
return ActivityType.Listening;
case "3":
case "WATCHING":
return ActivityType.Watching;
case "4":
case "CUSTOM":
return ActivityType.Custom;
case "5":
case "COMPETING":
return ActivityType.Competing;
default:
return undefined;
}
}
function envAsPresenceStatusData(key: string): PresenceStatusData | undefined {
key = key.toUpperCase();
const value = process.env[key]?.toLowerCase();
switch (value) {
case "online":
case "idle":
case "dnd":
case "invisible":
return value;
default:
return undefined;
}
}
const defaultConfig: IConfigRequired = { const defaultConfig: IConfigRequired = {
calendarParams: {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
tokens: { tokens: {
Discord: envAsString("TOKENS__DISCORD") ?? "", Discord: envAsString("TOKENS__DISCORD") ?? "",
OpenAI: envAsString("TOKENS__OPENAI") ?? "", OpenAI: envAsString("TOKENS__OPENAI") ?? "",
@ -123,16 +75,6 @@ const defaultConfig: IConfigRequired = {
}, },
]; ];
}, },
status: {
activities: isEnvDefined("STATUS__NAME") ? [{
name: envAsString("STATUS__NAME") as string,
type: envAsActivityType("STATUS__TYPE") ?? ActivityType.Custom,
state: envAsString("STATUS__STATE"),
url: envAsString("STATUS__URL"),
}] : undefined,
status: envAsPresenceStatusData("STATUS__STATUS"),
afk: envAsBoolean("STATUS__AFK"),
},
chatCompletionParams: { chatCompletionParams: {
model: envAsString("CHAT_COMPLETION_PARAMS__MODEL") ?? "gpt-3.5-turbo", model: envAsString("CHAT_COMPLETION_PARAMS__MODEL") ?? "gpt-3.5-turbo",
max_tokens: envAsNumber("CHAT_COMPLETION_PARAMS__MAX_TOKENS") ?? 384, max_tokens: envAsNumber("CHAT_COMPLETION_PARAMS__MAX_TOKENS") ?? 384,

View file

@ -1,10 +1,6 @@
import DiscordApi, { GuildTextBasedChannel, TextBasedChannel } from "discord.js"; import DiscordApi, { GuildTextBasedChannel, TextBasedChannel } from "discord.js";
import {APIError as OpenAIError} from "openai"; import { ChatCompletionRequestMessage, ChatCompletionResponseMessage } from "openai";
import { import Axios from "axios";
ChatCompletion,
ChatCompletionMessage,
ChatCompletionMessageParam
} from "openai/resources/chat";
import { database, openai, config } from "./index"; import { database, openai, config } from "./index";
import Moderation from "./moderation"; import Moderation from "./moderation";
@ -73,7 +69,7 @@ export function getAuthor(request: apiRequest) {
* @returns Promise of the done action * @returns Promise of the done action
*/ */
function requestReply( function requestReply(
request: apiRequest, request: RequestMessage,
message: DiscordApi.MessageReplyOptions & DiscordApi.InteractionReplyOptions, message: DiscordApi.MessageReplyOptions & DiscordApi.InteractionReplyOptions,
// TODO: add support for these below // TODO: add support for these below
replyOptions: DiscordApi.MessageReplyOptions = {}, replyOptions: DiscordApi.MessageReplyOptions = {},
@ -172,42 +168,29 @@ export async function queueRequest(request: apiRequest) {
* Logs used tokens to the terminal and to the database * Logs used tokens to the terminal and to the database
* @param answer the response that OpenAI returned * @param answer the response that OpenAI returned
* @param message the message that initiated the execution * @param message the message that initiated the execution
* @param functionRan counter of how many function have been ran (to distinct records in database) * @param functionRan counter of how many function have been ran
*/ */
function logUsedTokens( function logUsedTokens(
answer: ChatCompletion, answer: Awaited<ReturnType<typeof openai.createChatCompletion>>,
message: apiRequest | undefined = undefined, message: RequestMessage,
functionRan: number = 0, functionRan: number,
) { ) {
const usage = answer.usage; const usage = answer.data.usage;
const functionNames = const functionName = answer.data.choices[0].message?.function_call?.name;
answer.choices[0].message.tool_calls?.map(
v => v.type === "function" ? v.function.name : `[unknown type]`
);
if (usage !== undefined) { if (usage !== undefined) {
if (!message) { const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`;
// log usage to stdout even if we can't store it in database console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}${functionName ? " [Function: " + functionName + "]" : ""}`);
console.warn(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens from unknown call`);
// it doesn't make sense to store usage in database if we don't know where it came from
return;
}
const channelName: string = !message.channelId ? "[No channel]"
: !message.channel ? `[Unknown channel: ${message.channelId}]`
: !message.channel.isDMBased() ? `#${message.channel.name} (${message.guild?.name})`
: `#@${getAuthor(message).tag}`
;
console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in ${channelName}${functionNames && functionNames.length > 0 ? " [Tools: " + functionNames.join(", ") + "]" : ""}`);
database.usage.create({ database.usage.create({
data: { data: {
timestamp: message.createdAt, timestamp: message.createdAt,
user: BigInt(getAuthor(message).id), user: BigInt(getAuthor(message).id),
channel: BigInt(message.channelId ?? 0), channel: BigInt(message.channelId),
guild: message.guildId ? BigInt(message.guildId) : null, guild: message.guildId ? BigInt(message.guildId) : null,
usageRequest: usage.prompt_tokens, usageRequest: usage.prompt_tokens,
usageResponse: usage.completion_tokens, usageResponse: usage.completion_tokens,
functionName: functionNames?.join(", ") ?? null, functionName: functionName ?? null,
functionRan: functionRan, functionRan: functionName ? functionRan : 0,
} }
}).catch((e => { }).catch((e => {
console.error("Failed to push to a database"); console.error("Failed to push to a database");
@ -223,7 +206,8 @@ function logUsedTokens(
async function executeFromQueue(channel: string) { async function executeFromQueue(channel: string) {
const channelQueue = channelsRunning.get(channel) as ChannelsRunningValue; const channelQueue = channelsRunning.get(channel) as ChannelsRunningValue;
const message = channelQueue.at(0) as RequestMessage; const message = channelQueue.at(0) as RequestMessage;
let OpenAImessages: ChatCompletionMessageParam[] = []; let functionRanCounter = 0;
let OpenAImessages: ChatCompletionRequestMessage[] = [];
// ignore if we can't even send anything to reply // ignore if we can't even send anything to reply
if (!canReplyToRequest(message)) return; if (!canReplyToRequest(message)) return;
@ -250,46 +234,76 @@ async function executeFromQueue(channel: string) {
}); });
OpenAImessages = toOpenAIMessages(messages.values()); OpenAImessages = toOpenAIMessages(messages.values());
const answer = await executeChatCompletion(OpenAImessages, message); let generatedMessage: ChatCompletionResponseMessage | undefined = undefined;
let answer: Awaited<ReturnType<typeof openai.createChatCompletion>>;
do {
answer = await openai.createChatCompletion({
...config.chatCompletionParams,
messages: OpenAImessages,
// FIXME: don't use new instance of FunctionManager
functions: new FunctionManager().getFunctions(),
});
logUsedTokens(answer, message, ++functionRanCounter);
generatedMessage = answer.data.choices[0].message;
if (!generatedMessage) throw new Error("Empty message received");
// handle function calls
if (generatedMessage.function_call) {
OpenAImessages.push(generatedMessage);
// FIXME: don't use new instance of FunctionManager
OpenAImessages.push(
new FunctionManager().handleFunction(generatedMessage.function_call)
);
}
} while (generatedMessage.function_call);
channelQueue.stopTyping(); channelQueue.stopTyping();
const answerContent = answer.choices[0].message?.content; const answerContent = answer.data.choices[0].message?.content;
await replyInMultiMessage(answerContent, message); if (answerContent === undefined || answerContent === "") {
if (message instanceof DiscordApi.Message) message.react("😶").catch(() => {/* GRACEFAIL: It's okay if the bot won't reply */});
}
else {
const answerMessagesContent :string[] = [""];
for (const i of answerContent.split(/\n\n/)) {
if (answerMessagesContent[answerMessagesContent.length-1].length + i.length < 2000) {
answerMessagesContent[answerMessagesContent.length-1] += "\n\n" + i;
}
else {
answerMessagesContent.push(i);
}
}
for (const i of answerMessagesContent) {
const response = requestReply(message, {content: i}, {allowedMentions: { repliedUser: false }});
await response.then(rval => Moderation.checkMessageNoReturn(rval));
}
}
} catch (e) { } catch (e) {
let errorText: string = "";
channelQueue.stopTyping(); channelQueue.stopTyping();
if (typeof e !== "object") { console.error(`Error ocurred while handling chat completion request (${(e as object).constructor.name}):`);
console.error(`Error ocurred while handling chat completion request (${typeof e}):`);
console.error(e); console.error(e);
}
else if (e === null) {
console.error ("Error ocurred while handling chat completion request: null");
}
else {
console.error(`Error ocurred while handling chat completion request (${e.constructor.name}):`);
if (e instanceof OpenAIError) {
console.error(JSON.stringify(e));
}
else {
console.error(e);
}
if (OpenAImessages.length !== 0) { if (OpenAImessages.length !== 0) {
console.error("Messages:"); console.error("Messages:");
console.error(OpenAImessages); console.error(OpenAImessages);
} }
let errorText = "\n";
if (e instanceof Error) { if (e instanceof Error) {
errorText = e.message; errorText += e.message;
} }
else errorText = ""; else errorText = "";
if (e instanceof OpenAIError && e.code?.match(/^5..$/) && channelQueue.tries < 3) { if (Axios.isAxiosError(e) && e.code?.match(/^5..$/) && channelQueue.tries < 3) {
channelQueue.tries++; channelQueue.tries++;
await new Promise(r => setTimeout(r, 2000)); // pause for 2 seconds before retrying await new Promise(r => setTimeout(r, 2000)); // pause for 2 seconds before retrying
return executeFromQueue(channel); return executeFromQueue(channel);
} }
}
requestReply( requestReply(
message, message,
@ -310,70 +324,3 @@ async function executeFromQueue(channel: string) {
else else
return executeFromQueue(channel); return executeFromQueue(channel);
} }
/**
* Replies to a message and splits to multiple messages if needed.
* @param answerContent - The content of the answer.
* @param message - The request message to reply to.
*/
export async function replyInMultiMessage(answerContent: string | null, message: apiRequest) {
if (answerContent === null || answerContent === "") {
if (message instanceof DiscordApi.Message) message.react("😶").catch(() => { });
}
else {
const answerMessagesContent: string[] = [""];
for (const i of answerContent.split(/\n\n/)) {
if (answerMessagesContent[answerMessagesContent.length - 1].length + i.length < 2000) {
answerMessagesContent[answerMessagesContent.length - 1] += "\n\n" + i;
}
else {
answerMessagesContent.push(i);
}
}
for (const i of answerMessagesContent) {
const response = requestReply(message, { content: i }, { allowedMentions: { repliedUser: false } });
await response.then(rval => Moderation.checkMessageNoReturn(rval));
}
}
}
/**
* Executes the chat completion process.
*
* @param OpenAImessages An array of ChatCompletionMessageParam objects representing the messages for chat completion.
* @param message An optional RequestMessage object representing the request message, used for logging.
* @returns A Promise that resolves to the answer from the chat completion process.
*/
export async function executeChatCompletion(
OpenAImessages: ChatCompletionMessageParam[],
message: apiRequest | undefined,
) {
let generatedMessage: ChatCompletionMessage | undefined = undefined;
let answer: Awaited<ReturnType<typeof openai.chat.completions.create>>;
let functionRanCounter = 0;
do {
answer = await openai.chat.completions.create({
...config.chatCompletionParams,
messages: OpenAImessages,
// FIXME: don't use new instance of FunctionManager
tools: new FunctionManager().getToolsForOpenAi(),
});
functionRanCounter += answer.choices[0].message?.tool_calls?.length ?? 0;
logUsedTokens(answer, message, ++functionRanCounter);
generatedMessage = answer.choices[0].message;
if (!generatedMessage) throw new Error("Empty message received");
// handle tool calls
if (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0) {
OpenAImessages.push(generatedMessage);
// FIXME: don't use new instance of FunctionManager
OpenAImessages.push(...(await new FunctionManager().handleToolCalls(generatedMessage.tool_calls)));
}
} while (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0);
return answer;
}

View file

@ -1,43 +1,46 @@
import { FunctionDefinition } from "openai/resources"; import { ChatCompletionFunctions, ChatCompletionRequestMessage, ChatCompletionRequestMessageFunctionCall } from "openai";
import {
ChatCompletionMessageParam
, ChatCompletionMessageToolCall
, ChatCompletionTool
} from "openai/resources/chat";
import { type FromSchema, type JSONSchema } from "json-schema-to-ts";
type OpenAIFunctionRequestData = (JSONSchema & { import { config } from "./index";
type: "object"
});
type ChatCompletionToolDefinition = ChatCompletionTool; type parameterMap = {
type ChatCompletionToolCall = ChatCompletionMessageToolCall; string: string,
number: number,
};
type ChatCompletionFunctionDefinition = FunctionDefinition; type nameTypeMap = {[name: string]: keyof parameterMap} | Record<string, never>;
type OpenAIFunctionRequestData<T extends nameTypeMap> = {
[name in keyof T]: T[name];
};
/** /**
* Represents the function that can be ran by the OpenAI model * Represents the function that can be ran by the OpenAI model
*/ */
export interface OpenAIFunction< export interface OpenAIFunction<T extends nameTypeMap = nameTypeMap> {
T extends Readonly<OpenAIFunctionRequestData> = Readonly<OpenAIFunctionRequestData>
> {
name: string, name: string,
description?: string, description?: string,
parameters: T, parameters: {
type: "object",
properties: T extends Record<string, never> ? Record<string, never> : {
[name in T[string]]: {
type: T[name],
description?: string,
}
},
required?: Array<keyof T>,
},
} }
export abstract class OpenAIFunction< export abstract class OpenAIFunction<T extends nameTypeMap = nameTypeMap> {
T extends Readonly<OpenAIFunctionRequestData> = Readonly<OpenAIFunctionRequestData> getSettings(): ChatCompletionFunctions {
> {
getSettings(): ChatCompletionFunctionDefinition {
return { return {
name: this.name, name: this.name,
description: this.description, description: this.description,
parameters: this.parameters as Record<string, unknown>, parameters: this.parameters,
}; };
} }
abstract execute(data: FromSchema<T>): Promise<string>; abstract execute(data: OpenAIFunctionRequestData<T>): string;
} }
/* /*
@ -47,70 +50,60 @@ export default class FunctionManager {
store = new Map<string, OpenAIFunction>(); store = new Map<string, OpenAIFunction>();
constructor() { constructor() {
// TODO: import functions from functions directory this.store.set("getTime", new GetTime());
} }
public getTools(): ChatCompletionToolDefinition[] { public getFunctions(): ChatCompletionFunctions[] {
const rvalue: ChatCompletionToolDefinition[] = []; const rvalue: ChatCompletionFunctions[] = [];
for (const [, value] of this.store) { for (const [, value] of this.store) {
rvalue.push({type: "function", function: value.getSettings()}); rvalue.push(value.getSettings());
} }
return rvalue; return rvalue;
} }
public getToolsForOpenAi(): ChatCompletionTool[] | undefined { public handleFunction(request: ChatCompletionRequestMessageFunctionCall): ChatCompletionRequestMessage {
const rvalue = this.getTools();
return rvalue.length > 0 ? rvalue : undefined;
}
public handleFunction(request: ChatCompletionToolCall): Promise<ChatCompletionMessageParam> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let parsedArguments: any; let parsedArguments: any;
const functionToRun = this.store.get(request.function.name); const functionToRun = this.store.get(request.name ?? "");
// check if the function is registered // check if the function is registered
if (!functionToRun) { if (!functionToRun) {
return Promise.resolve({ return {
role: "system", role: "system",
content: `Only use functions that were provided to you (response for tool call ID: ${request.id})`, content: "Only use functions that were provided to you",
}); };
} }
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedArguments = JSON.parse(request.function.arguments); parsedArguments = JSON.parse(request.arguments ?? "");
} }
catch (e) { catch (e) {
console.error("Function arguments raw: " + request.function.arguments); console.error("Function arguments raw: " + request.arguments);
throw new Error(`Failed to parse the function JSON arguments when running function [${request.function.name}]`, {cause: e}); throw new Error(`Failed to parse the function JSON arguments when running function [${request.name}]`, {cause: e});
} }
// FIXME: Verify if the parsedArguments matches the requested function argument declaration. // FIXME: Verify if the parsedArguments matches the requested function argument declaration.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return functionToRun.execute(parsedArguments).then(content => {
return { return {
role: "tool", role: "function",
tool_call_id: request.id, name: request.name,
content: content, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
content: functionToRun.execute(parsedArguments),
}; };
}); }
} }
public handleToolCall(call: ChatCompletionToolCall): Promise<ChatCompletionMessageParam> { // buildins
if (call.type === "function") {
return this.handleFunction(call); class GetTime extends OpenAIFunction<Record<string, never>> {
} name = "getTime";
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions description = "Gets current date and time with a timezone attached";
throw new Error(`Unsupported tool call type: ${call.type || "never"}`); parameters = {
} type: "object" as const,
properties: {} as Record<string, never>,
public handleToolCalls(calls: ChatCompletionToolCall[]) { };
const rvalue: Promise<ChatCompletionMessageParam>[] = [];
for (const call of calls) { execute(): string {
if (call.type === "function") { return `${Intl.DateTimeFormat().resolvedOptions().timeZone}): ${new Date().toLocaleString("en-US", config.calendarParams)}`;
rvalue.push(this.handleToolCall(call));
}
}
return Promise.all(rvalue);
} }
} }

View file

@ -1,5 +1,5 @@
import DiscordApi from "discord.js"; import DiscordApi from "discord.js";
import OpenAIApi from "openai"; import { Configuration as OpenAIApiConfiguration, OpenAIApi } from "openai";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import Typescript from "typescript"; import Typescript from "typescript";
import fs from "node:fs"; import fs from "node:fs";
@ -11,7 +11,6 @@ const discord = new DiscordApi.Client({
intents: [ intents: [
DiscordApi.GatewayIntentBits.Guilds, DiscordApi.GatewayIntentBits.Guilds,
DiscordApi.GatewayIntentBits.GuildMessages, DiscordApi.GatewayIntentBits.GuildMessages,
DiscordApi.GatewayIntentBits.DirectMessages,
DiscordApi.GatewayIntentBits.MessageContent, DiscordApi.GatewayIntentBits.MessageContent,
] ]
}); });
@ -24,17 +23,9 @@ function getConfig() {
["./config.ts"], ["./config.ts"],
{outDir: "./dist"} {outDir: "./dist"}
); );
program.emit(program.getSourceFile("./config.ts"));
program.getSourceFiles()
.filter(e => {
if (!e.fileName.match(`^${process.cwd()}`)) return true;
if (e.fileName.match(`${process.cwd()}/node_modules`)) return false;
if (e.fileName.match(`${process.cwd()}/src/`)) return false;
return true;
})
.forEach(e => program.emit(e));
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
fileConfig = require("../config").default as IConfig; fileConfig = require("./config").default as IConfig;
} catch (e) { } catch (e) {
//FIXME: make errors more descriptive to the enduser //FIXME: make errors more descriptive to the enduser
console.log(e); console.log(e);
@ -44,9 +35,9 @@ function getConfig() {
export const config: IConfigRequired = getConfig(); export const config: IConfigRequired = getConfig();
export const openai = new OpenAIApi({ export const openai = new OpenAIApi(new OpenAIApiConfiguration({
apiKey: config.tokens.OpenAI apiKey: config.tokens.OpenAI
}); }));
export const database = new PrismaClient(); export const database = new PrismaClient();
@ -55,29 +46,13 @@ interactionManager.bindClient(discord);
discord.on("ready", event => { discord.on("ready", event => {
console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`); console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`);
event.user.setPresence(config.status);
}); });
discord.on("messageCreate", message => { discord.on("messageCreate", message => {
if (message.author.bot) return; if (message.author.bot) return;
if (!message.channel.isDMBased()) { if (!message.mentions.has(message.client.user)) return;
if (!message.mentions.has(message.client.user, { ignoreEveryone: true })) return;
}
return queueRequest(message); return queueRequest(message);
}); });
if (require.main === module) { if (require.main === module) void discord.login(config.tokens.Discord);
void discord.login(config.tokens.Discord);
process.on("SIGINT", () => {
console.log("got SIGINT, exiting");
//FIXME: finish executing requests then exit
discord.destroy()
.then(() => process.exit())
.catch((e) => {
console.error("Failed to gracefully exit");
console.error(e);
process.exit();
});
});
}

View file

@ -34,9 +34,8 @@ export default class CommandManager {
interaction.reply({ interaction.reply({
embeds: [{ embeds: [{
color: 0xff0000, color: 0xff0000,
description: `Failed to perform interaction:\n\`\`${e}\`\``, description: `Failed to perform interaction:\n\`${e}\``,
}], }]
ephemeral: true,
}).catch(() => {/* NOTE: We're still logging the issue that happened to the console */}); }).catch(() => {/* NOTE: We're still logging the issue that happened to the console */});
console.error(`Failed to perform interaction: ${interaction.commandName}`); console.error(`Failed to perform interaction: ${interaction.commandName}`);
console.error(e); console.error(e);

View file

@ -28,11 +28,11 @@ export default class Moderation {
} }
} }
const answer = await openai.moderations.create({ const answer = await openai.createModeration({
input: await formatRequestOrResponse(message), input: await formatRequestOrResponse(message),
}); });
const flagged = answer.results[0].flagged; const flagged = answer.data.results[0].flagged;
this.cache.set(message.id, flagged); this.cache.set(message.id, flagged);
// FIXME: These next 7 lines does not belong there and should be refactored out. // FIXME: These next 7 lines does not belong there and should be refactored out.
if (flagged) if (message instanceof Message) { if (flagged) if (message instanceof Message) {

View file

@ -12,19 +12,16 @@ import { Usage } from "@prisma/client";
export default class tokenCount implements IQuota { export default class tokenCount implements IQuota {
defaultQuota: number; defaultQuota: number;
lookback: number; lookback: number;
requestTokenMultiplier: number; considerInputTokensAsHalf: boolean;
responseTokenMultiplier: number;
constructor( constructor(
defaultQuota: number = 512 * 25, defaultQuota: number = 512 * 25,
lookback: number = 1000 * 60 * 60 * 24, lookback: number = 1000 * 60 * 60 * 24,
requestTokenMultiplier: number = 1, considerInputTokensAsHalf: boolean = true,
responseTokenMultiplier: number = 1,
) { ) {
this.defaultQuota = defaultQuota; this.defaultQuota = defaultQuota;
this.lookback = lookback; this.lookback = lookback;
this.requestTokenMultiplier = requestTokenMultiplier; this.considerInputTokensAsHalf = considerInputTokensAsHalf;
this.responseTokenMultiplier = responseTokenMultiplier;
} }
private getUserQuota(id: string) { private getUserQuota(id: string) {
@ -54,11 +51,12 @@ export default class tokenCount implements IQuota {
} }
}))._sum; }))._sum;
const usageRequest = usedTokens.usageRequest === null ? 0 : usedTokens.usageRequest; if (!usedTokens.usageRequest || !usedTokens.usageResponse) throw new Error("Null from a database!! (tokenCount Quota)");
const usageResponse = usedTokens.usageResponse === null ? 0 : usedTokens.usageResponse;
const usedUnits = (() => { const usedUnits = (() => {
return usageRequest * this.requestTokenMultiplier + usageResponse * this.responseTokenMultiplier; if (this.considerInputTokensAsHalf)
return usedTokens.usageResponse + usedTokens.usageRequest / 2;
return usedTokens.usageResponse + usedTokens.usageRequest;
})(); })();
if (userQuota?.vip) return this.createUserQuotaData(Infinity, usedUnits); if (userQuota?.vip) return this.createUserQuotaData(Infinity, usedUnits);
@ -75,13 +73,30 @@ export default class tokenCount implements IQuota {
* @returns promise of giving out the record * @returns promise of giving out the record
*/ */
findNthUsage(user: string, requestTimestamp: number, unitCount: number) { findNthUsage(user: string, requestTimestamp: number, unitCount: number) {
return database.$queryRaw<Array<Usage & {usage: number | bigint}>>` if (this.considerInputTokensAsHalf)
return database.$queryRaw<Array<Usage & {usage: number}>>`
SELECT t1.*, ( SELECT t1.*, (
SELECT SELECT
SUM( SUM(usageResponse + usageRequest/2) AS usage
usageRequest * ${this.requestTokenMultiplier} + FROM \`usage\`
usageResponse * ${this.responseTokenMultiplier} WHERE
) AS usage user = ${user} AND
timestamp >= ${requestTimestamp - this.lookback} AND
timestamp <= t1.timestamp
) as usage
FROM
\`usage\` AS t1
WHERE
user = ${user} AND
timestamp >= ${requestTimestamp - this.lookback} AND
usage >= ${unitCount}
ORDER BY timestamp ASC
LIMIT 1
`;
return database.$queryRaw<Array<Usage & {usage: bigint}>>`
SELECT t1.*, (
SELECT
SUM(usageResponse + usageRequest) AS usage
FROM \`usage\` FROM \`usage\`
WHERE WHERE
user = ${user} AND user = ${user} AND
@ -105,14 +120,14 @@ export default class tokenCount implements IQuota {
): Promise<userQuotaRecoveryData> { ): Promise<userQuotaRecoveryData> {
const userId = typeof user ==="string" ? user : user.id; const userId = typeof user ==="string" ? user : user.id;
const [userQuota, overUnitCountRecord] = await Promise.all([ const [userQuota, renameMebecause] = await Promise.all([
this.checkUser(userId, request), this.checkUser(userId, request),
this.findNthUsage(userId, request.createdTimestamp, unitCount) this.findNthUsage(userId, request.createdTimestamp, unitCount)
]); ]);
return { return {
...userQuota, ...userQuota,
recoveryTimestamp: (overUnitCountRecord.at(0)?.timestamp.valueOf() ?? Infinity) + this.lookback, recoveryTimestamp: (renameMebecause.at(0)?.timestamp.valueOf() ?? Infinity) + this.lookback,
}; };
} }

View file

@ -0,0 +1,42 @@
// https://discordjs.guide/creating-your-bot/command-deployment.html#guild-commands
import { REST, RESTGetAPIOAuth2CurrentApplicationResult, RESTPostAPIApplicationCommandsJSONBody, Routes } from "discord.js";
import { config } from "../index";
import requireDirectory from "require-directory";
import Command from "../command";
const post: RESTPostAPIApplicationCommandsJSONBody[] = [];
const guildId = process.argv.slice(2)[0];
requireDirectory<{default: Command}, void>(module, "../commands", {
visit: function (obj) {
console.log(obj);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
post.push(new obj.default().toRESTPostApplicationCommands());
},
});
const rest = new REST().setToken(config.tokens.Discord);
(async () => {
const me = await rest.get(Routes.oauth2CurrentApplication()) as RESTGetAPIOAuth2CurrentApplicationResult;
if (guildId && guildId !== "") {
console.log(`Started refreshing ${post.length} application guild (${guildId}) commands.`);
await rest.put(
Routes.applicationGuildCommands(me.id, guildId),
{ body: post },
);
} else {
console.log(`Started refreshing ${post.length} application global commands.`);
await rest.put(
Routes.applicationCommands(me.id),
{ body: post },
);
}
console.log("Refreshed successfully");
})().catch( e => {
console.error(e);
});

View file

@ -1,4 +1,4 @@
import { ChatCompletionMessageParam as OpenAIMessage } from "openai/resources/chat"; import { ChatCompletionRequestMessage as OpenAIMessage } from "openai";
import { Collection, Message as DiscordMessage, InteractionResponse } from "discord.js"; import { Collection, Message as DiscordMessage, InteractionResponse } from "discord.js";
import FoldToAscii from "fold-to-ascii"; import FoldToAscii from "fold-to-ascii";
@ -63,7 +63,7 @@ export function formatMessage(message: DiscordMessage): string {
* @param name the name to format * @param name the name to format
* @returns formatted name * @returns formatted name
*/ */
export function formatName(name: string): string { function formatName(name: string): string {
// replace all characters to ascii equivelant // replace all characters to ascii equivelant
return FoldToAscii.foldReplacing(name) return FoldToAscii.foldReplacing(name)
// White spaces are not allowed // White spaces are not allowed
@ -88,8 +88,7 @@ function getAuthorUsername(message: DiscordMessage): string | undefined {
if (name.length >= 3) return name; if (name.length >= 3) return name;
} }
const name = formatName(message.author.username); const name = formatName(message.author.username);
if (name.length > 0) return name; return name;
return message.author.id;
} }
/** /**

View file

@ -1,14 +1,13 @@
{ {
"include": [ "include": [
"./src/**/*", "./src/**/*"
"./scripts/**/*"
], ],
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "CommonJS", "module": "CommonJS",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist/", "outDir": "./dist/",
"rootDir": "./", "rootDir": "./src/",
"strict": true, "strict": true,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,