Compare commits

...

41 commits

Author SHA1 Message Date
1b402c791c execution: log channel ID to terminal if it's unknown
channelId is set for commands executed outside of common guilds
no changes for usage logging in database is required
2024-04-26 08:13:29 +02:00
c4edf55f65 commands/ask: enforce userLimit 2024-04-26 08:07:46 +02:00
6f5f425166 configDefault: add status options 2024-04-26 07:14:41 +02:00
b1e464fe50 Dockerfile: create db directory 2024-04-26 06:14:57 +02:00
6efb6e5876 commands/ask: create
Allows to interact with the bot when bot is user installed in discord.
2024-04-26 05:41:53 +02:00
9f5dfefb31 execution: support for requests that don't have channel set
Interactions initiated outside of servers where bot is don't have channel assigned
2024-04-26 05:41:53 +02:00
2fab1b1b42 execution: factor out replying code to it's own fuction 2024-04-26 05:17:32 +02:00
482f72a4d1 execution: factor out chat completion process 2024-04-26 04:03:34 +02:00
d3567c3607 execution: handle undefined message in logUsedTokens 2024-04-26 04:02:09 +02:00
67a6e4d486 execution+configDefault: retrofit for tooll_calls api 2024-04-26 03:27:15 +02:00
370b7623b5 Dockerfile: chmod dist to 777 for config.ts compilation 2024-04-24 02:38:52 +02:00
b567e13f2a functionManager: use json-schema-to-ts to derive arguments for OpenAIFunctions 2024-04-24 02:27:31 +02:00
d9bee2dcf2 check-quota: define integration types and context to any 2024-04-23 21:11:40 +02:00
3c10f4ed6f Allow chatting with bot through direct messages
Added new command to make the bot see direct messages,
because by default these are not visible,
unless bot opens direct message with the user.
2024-04-23 21:10:52 +02:00
63cb52e7f4 Command: Add integration and context types
While discord api supports that, discord api types, which discord.js depends on does not.
2024-04-23 20:56:55 +02:00
0e5c8d22cc functionManager: retrofit for tool calls api 2024-04-23 17:46:24 +02:00
91232e99a7 Update dependencies 2024-04-23 16:47:08 +02:00
dc01146ee8 ignore @everyone mentions 2023-12-25 16:09:35 +01:00
74359067d0 functionManager: don't return empty function array for openai
openai doesn't accept empty function parameter,
it was discovered after I removed the testing buildin function
2023-12-13 21:08:09 +01:00
24e85f535a "Gracefully" exit when receiving sigint 2023-12-13 20:56:23 +01:00
02a5b0f7b9 Remove getTime model function
it lead to higher usage of tokens.
2023-12-13 17:42:59 +01:00
722862ded3 Add shell.nix
to development util on nixos system
2023-12-13 17:06:09 +01:00
c911e567c6 Update dependencies 2023-12-13 17:04:18 +01:00
6e1cb8c956 Merge branch 'refactor/scripts' 2023-10-12 12:01:52 +02:00
a186ba9e80 scripts/pushCommands: Refactor how commands are being read
this should reduce errors when reading commands
also this won't fail if reading one command fails
2023-10-12 11:55:53 +02:00
a05047ab7d script/pushCommands: make sure the script exits 2023-09-30 14:47:10 +02:00
4729f7f563 /check-limit: rename to check-quota and change description
to match it to the new reality, limits are named quotas now

description of an option previously was longer than 100 characters
which discord api didn't like it
2023-09-30 14:46:12 +02:00
6a31473d22 scripts: move scripts to the project root directory
I believe, that these kind of scripts should reside there
instead of src directory
2023-09-30 14:46:12 +02:00
b7f6a5fe91 toOpenAIMessages: Add fallback to use user id on empty resolved name
Previously if someone named whose current mechanism
resolved to empty string ("Джо" for example)
then the openai would scream that the name
does not match their server side regex.

Meaning that the bot could not be used
in a given channel until that person
changes it's display name
or it will be forgotten for the bot.
2023-09-27 20:45:23 +02:00
ffa79e798e Fix import grouping in execution.ts
I'm silly silly bad cutie
that doesn't check commits
when I push them
2023-09-27 20:30:08 +02:00
23ceca5cd3 Update OpenAI dependency 2023-09-27 17:14:17 +02:00
8ed2e758f8 Update dependencies
OpenAI package will be updated soon,
as it needs codebase migration
2023-09-27 15:52:30 +02:00
03a1c62cd5 Execution: Stringify response error on axios error
I'm sick of not understanding the 400 problem
2023-09-26 18:17:12 +02:00
6272c7f551 /check-limit: change description of recovery-for paramteter
to mach it better for the current default behaviour
2023-09-25 11:32:52 +02:00
49b074f98e /check-limit: show when the user can use the bot again by default
fixes #12
2023-09-25 11:10:21 +02:00
80f4f18eab quota/tokenCount: allow arbitrary multipliers for tokens
I didn't notice that OpenAI changed pricing again
2023-09-25 09:58:54 +02:00
552143e345 quota/tokenCount: Rename single variable name to something that has more sense
I forgot to rename it when I commited it last time ;-;
2023-09-25 09:30:52 +02:00
94992743e8 quota/tokenCount: fix "null from a database" 2023-09-21 21:51:03 +02:00
2629659ffc Interactions: fix on failed reply to be ephemeral
fixes #17
2023-09-21 20:57:40 +02:00
96dd7bce95 Merge branch 'feature/quota' 2023-09-21 20:52:06 +02:00
18646b9dc6 config: fix imports not working correctly
this patch moves the rootDir of the typescript project up a directory
this moves all content in the dist directory inside the new src directory

I couldn't find other way
2023-09-21 20:51:38 +02:00
19 changed files with 1342 additions and 740 deletions

View file

@ -12,7 +12,20 @@ RUN npx prisma generate
# Typescript compiling
COPY tsconfig.json .
COPY src ./src
COPY scripts ./scripts
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
CMD ["node", "dist/index.js"]
CMD ["node", "dist/src/index.js"]
STOPSIGNAL SIGINT

1128
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

66
scripts/pushCommands.ts Normal file
View file

@ -0,0 +1,66 @@
// 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);
});

78
shell.nix Normal file
View file

@ -0,0 +1,78 @@
{ 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,23 +1,61 @@
import { AutocompleteInteraction, PermissionsBitField } from "discord.js";
import { RESTPostAPIApplicationCommandsJSONBody } from "discord.js";
import { ApplicationCommandOption, ApplicationCommandType, ChatInputCommandInteraction, LocalizationMap, MessageInteraction, PermissionResolvable, UserSelectMenuInteraction } from "discord.js";
import { APIApplicationCommandOption, ApplicationCommandType, ChatInputCommandInteraction, LocalizationMap, MessageInteraction, PermissionResolvable, UserSelectMenuInteraction } from "discord.js";
type InteractionTypeMap = {
export type InteractionTypeMap = {
// [CommandType]: [Interaction, Description]
[ApplicationCommandType.ChatInput]: [ChatInputCommandInteraction, string];
[ApplicationCommandType.Message]: [MessageInteraction, 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> {
readonly name: string;
readonly name_localizations?: LocalizationMap;
readonly description: InteractionTypeMap[Type][1];
readonly description_localizations?: LocalizationMap;
readonly options?: ApplicationCommandOption[];
readonly options?: APIApplicationCommandOption[];
readonly default_member_permissions?: PermissionResolvable;
readonly type: Type;
readonly nsfw?: boolean;
/** @deprecated use contexts instead */
readonly dm_permission?: boolean;
readonly integration_types?: ApplicationIntegrationType[];
readonly contexts?: InteractionContextTypes[];
}
abstract class Command<Type extends keyof InteractionTypeMap = ApplicationCommandType> {
@ -25,7 +63,7 @@ abstract class Command<Type extends keyof InteractionTypeMap = ApplicationComman
autocomplete?(interaction: Type extends ApplicationCommandType.ChatInput ? AutocompleteInteraction : never ): Promise<void>;
toRESTPostApplicationCommands(): RESTPostAPIApplicationCommandsJSONBody {
toRESTPostApplicationCommands(): FutureRESTPostAPIApplicationCommandsJSONBody {
return {
name: this.name,
name_localizations: this.name_localizations,
@ -36,6 +74,8 @@ abstract class Command<Type extends keyof InteractionTypeMap = ApplicationComman
type: this.type,
nsfw: this.nsfw,
dm_permission: this.dm_permission,
integration_types: this.integration_types,
contexts: this.contexts,
};
}
}

90
src/commands/ask.ts Normal file
View file

@ -0,0 +1,90 @@
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,16 +1,20 @@
import { ApplicationCommandType, ChatInputCommandInteraction, ApplicationCommandOption, ApplicationCommandOptionType, APIEmbedField } from "discord.js";
import { ApplicationCommandType, ChatInputCommandInteraction, APIApplicationCommandOption, ApplicationCommandOptionType, APIEmbedField } from "discord.js";
import Command from "../command";
import
Command
,{ApplicationIntegrationType
, InteractionContextTypes
} from "../command";
import { config } from "../index";
export default class MyLimit extends Command implements Command {
name = "check-limit";
description = "Checks your limit and usage";
name = "check-quota";
description = "Checks your quota and usage";
type = ApplicationCommandType.ChatInput;
options: ApplicationCommandOption[] = [
options: APIApplicationCommandOption[] = [
{
name: "recovery-for",
description: "Calculate the limit recovery time for given message count (default 1)",
description: "Get the recovery time for given quota units count (default: until can use the bot or 1)",
type: ApplicationCommandOptionType.Integer,
required: false,
},
@ -18,13 +22,34 @@ export default class MyLimit extends Command implements Command {
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) {
let recoveryFor = interaction.options.getInteger("recovery-for", false) ?? 1;
let recoveryFor = interaction.options.getInteger("recovery-for", false);
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;
const userQuotaRecovery = await config.quota.getUserQuotaRecovery(interaction.user, interaction, recoveryFor);

26
src/commands/listen.ts Normal file
View file

@ -0,0 +1,26 @@
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,23 +1,29 @@
import { Message } from "discord.js";
import {
ChatCompletionRequestMessage as OpenAIMessage,
CreateChatCompletionRequest as ChatCompletionRequestData,
} from "openai";
ActivityType
, type PresenceStatusData
, type PresenceData
} from "discord.js";
import {
ChatCompletionMessageParam as OpenAIMessage,
ChatCompletionCreateParamsNonStreaming as ChatCompletionRequestData,
} from "openai/resources/chat";
import IQuota from "./IQuota";
import MessageCount from "./quota/messageCount";
import { apiRequest } from "./execution";
export interface IConfigRequired {
readonly calendarParams: Intl.DateTimeFormatOptions;
/** Tokens to authentiate with */
readonly tokens: {
readonly Discord: string;
readonly OpenAI: string;
};
/** Discord bot status */
readonly status: PresenceData
/** Messages to append at the start of every chat history when sending to API */
systemPrompt(context: Message): OpenAIMessage[];
systemPrompt(context: apiRequest): OpenAIMessage[];
/** OpenAI model config */
readonly chatCompletionParams: Omit<ChatCompletionRequestData, "messages" | "function_call" | "functions" | "n">;
readonly chatCompletionParams: Omit<ChatCompletionRequestData, "messages" | "function_call" | "tool_call" | "functions" | "n">;
/** Limits for message selection */
readonly readLimits: {
/** Maximum message age to include (in miliseconds) */
@ -40,6 +46,10 @@ export default function newConfig(config?: IConfig): IConfigRequired {
return { ...defaultConfig, ...config };
}
function isEnvDefined(key: string): boolean {
return process.env[key] !== undefined;
}
function envAsString(key: string): string | undefined {
key = key.toLocaleUpperCase();
return process.env[key];
@ -51,16 +61,54 @@ function envAsNumber(key: string): number | 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 = {
calendarParams: {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
tokens: {
Discord: envAsString("TOKENS__DISCORD") ?? "",
OpenAI: envAsString("TOKENS__OPENAI") ?? "",
@ -75,6 +123,16 @@ 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: {
model: envAsString("CHAT_COMPLETION_PARAMS__MODEL") ?? "gpt-3.5-turbo",
max_tokens: envAsNumber("CHAT_COMPLETION_PARAMS__MAX_TOKENS") ?? 384,

View file

@ -1,6 +1,10 @@
import DiscordApi, { GuildTextBasedChannel, TextBasedChannel } from "discord.js";
import { ChatCompletionRequestMessage, ChatCompletionResponseMessage } from "openai";
import Axios from "axios";
import {APIError as OpenAIError} from "openai";
import {
ChatCompletion,
ChatCompletionMessage,
ChatCompletionMessageParam
} from "openai/resources/chat";
import { database, openai, config } from "./index";
import Moderation from "./moderation";
@ -69,7 +73,7 @@ export function getAuthor(request: apiRequest) {
* @returns Promise of the done action
*/
function requestReply(
request: RequestMessage,
request: apiRequest,
message: DiscordApi.MessageReplyOptions & DiscordApi.InteractionReplyOptions,
// TODO: add support for these below
replyOptions: DiscordApi.MessageReplyOptions = {},
@ -168,29 +172,42 @@ export async function queueRequest(request: apiRequest) {
* Logs used tokens to the terminal and to the database
* @param answer the response that OpenAI returned
* @param message the message that initiated the execution
* @param functionRan counter of how many function have been ran
* @param functionRan counter of how many function have been ran (to distinct records in database)
*/
function logUsedTokens(
answer: Awaited<ReturnType<typeof openai.createChatCompletion>>,
message: RequestMessage,
functionRan: number,
answer: ChatCompletion,
message: apiRequest | undefined = undefined,
functionRan: number = 0,
) {
const usage = answer.data.usage;
const functionName = answer.data.choices[0].message?.function_call?.name;
const usage = answer.usage;
const functionNames =
answer.choices[0].message.tool_calls?.map(
v => v.type === "function" ? v.function.name : `[unknown type]`
);
if (usage !== undefined) {
const channelName: string = !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}${functionName ? " [Function: " + functionName + "]" : ""}`);
if (!message) {
// log usage to stdout even if we can't store it in database
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({
data: {
timestamp: message.createdAt,
user: BigInt(getAuthor(message).id),
channel: BigInt(message.channelId),
channel: BigInt(message.channelId ?? 0),
guild: message.guildId ? BigInt(message.guildId) : null,
usageRequest: usage.prompt_tokens,
usageResponse: usage.completion_tokens,
functionName: functionName ?? null,
functionRan: functionName ? functionRan : 0,
functionName: functionNames?.join(", ") ?? null,
functionRan: functionRan,
}
}).catch((e => {
console.error("Failed to push to a database");
@ -206,8 +223,7 @@ function logUsedTokens(
async function executeFromQueue(channel: string) {
const channelQueue = channelsRunning.get(channel) as ChannelsRunningValue;
const message = channelQueue.at(0) as RequestMessage;
let functionRanCounter = 0;
let OpenAImessages: ChatCompletionRequestMessage[] = [];
let OpenAImessages: ChatCompletionMessageParam[] = [];
// ignore if we can't even send anything to reply
if (!canReplyToRequest(message)) return;
@ -234,75 +250,45 @@ async function executeFromQueue(channel: string) {
});
OpenAImessages = toOpenAIMessages(messages.values());
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);
const answer = await executeChatCompletion(OpenAImessages, message);
channelQueue.stopTyping();
const answerContent = answer.data.choices[0].message?.content;
const answerContent = answer.choices[0].message?.content;
if (answerContent === undefined || answerContent === "") {
if (message instanceof DiscordApi.Message) message.react("😶").catch(() => {/* GRACEFAIL: It's okay if the bot won't reply */});
await replyInMultiMessage(answerContent, message);
} catch (e) {
let errorText: string = "";
channelQueue.stopTyping();
if (typeof e !== "object") {
console.error(`Error ocurred while handling chat completion request (${typeof e}):`);
console.error(e);
}
else if (e === null) {
console.error ("Error ocurred while handling chat completion request: null");
}
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);
}
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) {
console.error("Messages:");
console.error(OpenAImessages);
}
for (const i of answerMessagesContent) {
const response = requestReply(message, {content: i}, {allowedMentions: { repliedUser: false }});
await response.then(rval => Moderation.checkMessageNoReturn(rval));
if (e instanceof Error) {
errorText = e.message;
}
else errorText = "";
if (e instanceof OpenAIError && e.code?.match(/^5..$/) && channelQueue.tries < 3) {
channelQueue.tries++;
await new Promise(r => setTimeout(r, 2000)); // pause for 2 seconds before retrying
return executeFromQueue(channel);
}
}
} catch (e) {
channelQueue.stopTyping();
console.error(`Error ocurred while handling chat completion request (${(e as object).constructor.name}):`);
console.error(e);
if (OpenAImessages.length !== 0) {
console.error("Messages:");
console.error(OpenAImessages);
}
let errorText = "\n";
if (e instanceof Error) {
errorText += e.message;
}
else errorText = "";
if (Axios.isAxiosError(e) && e.code?.match(/^5..$/) && channelQueue.tries < 3) {
channelQueue.tries++;
await new Promise(r => setTimeout(r, 2000)); // pause for 2 seconds before retrying
return executeFromQueue(channel);
}
requestReply(
@ -324,3 +310,70 @@ async function executeFromQueue(channel: string) {
else
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,46 +1,43 @@
import { ChatCompletionFunctions, ChatCompletionRequestMessage, ChatCompletionRequestMessageFunctionCall } from "openai";
import { FunctionDefinition } from "openai/resources";
import {
ChatCompletionMessageParam
, ChatCompletionMessageToolCall
, ChatCompletionTool
} from "openai/resources/chat";
import { type FromSchema, type JSONSchema } from "json-schema-to-ts";
import { config } from "./index";
type OpenAIFunctionRequestData = (JSONSchema & {
type: "object"
});
type parameterMap = {
string: string,
number: number,
};
type ChatCompletionToolDefinition = ChatCompletionTool;
type ChatCompletionToolCall = ChatCompletionMessageToolCall;
type nameTypeMap = {[name: string]: keyof parameterMap} | Record<string, never>;
type OpenAIFunctionRequestData<T extends nameTypeMap> = {
[name in keyof T]: T[name];
};
type ChatCompletionFunctionDefinition = FunctionDefinition;
/**
* Represents the function that can be ran by the OpenAI model
*/
export interface OpenAIFunction<T extends nameTypeMap = nameTypeMap> {
export interface OpenAIFunction<
T extends Readonly<OpenAIFunctionRequestData> = Readonly<OpenAIFunctionRequestData>
> {
name: string,
description?: string,
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>,
},
parameters: T,
}
export abstract class OpenAIFunction<T extends nameTypeMap = nameTypeMap> {
getSettings(): ChatCompletionFunctions {
export abstract class OpenAIFunction<
T extends Readonly<OpenAIFunctionRequestData> = Readonly<OpenAIFunctionRequestData>
> {
getSettings(): ChatCompletionFunctionDefinition {
return {
name: this.name,
description: this.description,
parameters: this.parameters,
parameters: this.parameters as Record<string, unknown>,
};
}
abstract execute(data: OpenAIFunctionRequestData<T>): string;
abstract execute(data: FromSchema<T>): Promise<string>;
}
/*
@ -50,60 +47,70 @@ export default class FunctionManager {
store = new Map<string, OpenAIFunction>();
constructor() {
this.store.set("getTime", new GetTime());
// TODO: import functions from functions directory
}
public getFunctions(): ChatCompletionFunctions[] {
const rvalue: ChatCompletionFunctions[] = [];
public getTools(): ChatCompletionToolDefinition[] {
const rvalue: ChatCompletionToolDefinition[] = [];
for (const [, value] of this.store) {
rvalue.push(value.getSettings());
rvalue.push({type: "function", function: value.getSettings()});
}
return rvalue;
}
public handleFunction(request: ChatCompletionRequestMessageFunctionCall): ChatCompletionRequestMessage {
public getToolsForOpenAi(): ChatCompletionTool[] | undefined {
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
let parsedArguments: any;
const functionToRun = this.store.get(request.name ?? "");
const functionToRun = this.store.get(request.function.name);
// check if the function is registered
if (!functionToRun) {
return {
return Promise.resolve({
role: "system",
content: "Only use functions that were provided to you",
};
content: `Only use functions that were provided to you (response for tool call ID: ${request.id})`,
});
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedArguments = JSON.parse(request.arguments ?? "");
parsedArguments = JSON.parse(request.function.arguments);
}
catch (e) {
console.error("Function arguments raw: " + request.arguments);
throw new Error(`Failed to parse the function JSON arguments when running function [${request.name}]`, {cause: e});
console.error("Function arguments raw: " + request.function.arguments);
throw new Error(`Failed to parse the function JSON arguments when running function [${request.function.name}]`, {cause: e});
}
// FIXME: Verify if the parsedArguments matches the requested function argument declaration.
return {
role: "function",
name: request.name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
content: functionToRun.execute(parsedArguments),
};
}
}
// buildins
class GetTime extends OpenAIFunction<Record<string, never>> {
name = "getTime";
description = "Gets current date and time with a timezone attached";
parameters = {
type: "object" as const,
properties: {} as Record<string, never>,
};
execute(): string {
return `${Intl.DateTimeFormat().resolvedOptions().timeZone}): ${new Date().toLocaleString("en-US", config.calendarParams)}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return functionToRun.execute(parsedArguments).then(content => {
return {
role: "tool",
tool_call_id: request.id,
content: content,
};
});
}
public handleToolCall(call: ChatCompletionToolCall): Promise<ChatCompletionMessageParam> {
if (call.type === "function") {
return this.handleFunction(call);
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unsupported tool call type: ${call.type || "never"}`);
}
public handleToolCalls(calls: ChatCompletionToolCall[]) {
const rvalue: Promise<ChatCompletionMessageParam>[] = [];
for (const call of calls) {
if (call.type === "function") {
rvalue.push(this.handleToolCall(call));
}
}
return Promise.all(rvalue);
}
}

View file

@ -1,5 +1,5 @@
import DiscordApi from "discord.js";
import { Configuration as OpenAIApiConfiguration, OpenAIApi } from "openai";
import OpenAIApi from "openai";
import { PrismaClient } from "@prisma/client";
import Typescript from "typescript";
import fs from "node:fs";
@ -11,6 +11,7 @@ const discord = new DiscordApi.Client({
intents: [
DiscordApi.GatewayIntentBits.Guilds,
DiscordApi.GatewayIntentBits.GuildMessages,
DiscordApi.GatewayIntentBits.DirectMessages,
DiscordApi.GatewayIntentBits.MessageContent,
]
});
@ -23,9 +24,17 @@ function getConfig() {
["./config.ts"],
{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
fileConfig = require("./config").default as IConfig;
fileConfig = require("../config").default as IConfig;
} catch (e) {
//FIXME: make errors more descriptive to the enduser
console.log(e);
@ -35,9 +44,9 @@ function getConfig() {
export const config: IConfigRequired = getConfig();
export const openai = new OpenAIApi(new OpenAIApiConfiguration({
export const openai = new OpenAIApi({
apiKey: config.tokens.OpenAI
}));
});
export const database = new PrismaClient();
@ -46,13 +55,29 @@ interactionManager.bindClient(discord);
discord.on("ready", event => {
console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`);
event.user.setPresence(config.status);
});
discord.on("messageCreate", message => {
if (message.author.bot) return;
if (!message.mentions.has(message.client.user)) return;
if (!message.channel.isDMBased()) {
if (!message.mentions.has(message.client.user, { ignoreEveryone: true })) return;
}
return queueRequest(message);
});
if (require.main === module) void discord.login(config.tokens.Discord);
if (require.main === module) {
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,8 +34,9 @@ export default class CommandManager {
interaction.reply({
embeds: [{
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 */});
console.error(`Failed to perform interaction: ${interaction.commandName}`);
console.error(e);

View file

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

View file

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

View file

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

View file

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