Compare commits
41 commits
feature/qu
...
main
Author | SHA1 | Date | |
---|---|---|---|
1b402c791c | |||
c4edf55f65 | |||
6f5f425166 | |||
b1e464fe50 | |||
6efb6e5876 | |||
9f5dfefb31 | |||
2fab1b1b42 | |||
482f72a4d1 | |||
d3567c3607 | |||
67a6e4d486 | |||
370b7623b5 | |||
b567e13f2a | |||
d9bee2dcf2 | |||
3c10f4ed6f | |||
63cb52e7f4 | |||
0e5c8d22cc | |||
91232e99a7 | |||
dc01146ee8 | |||
74359067d0 | |||
24e85f535a | |||
02a5b0f7b9 | |||
722862ded3 | |||
c911e567c6 | |||
6e1cb8c956 | |||
a186ba9e80 | |||
a05047ab7d | |||
4729f7f563 | |||
6a31473d22 | |||
b7f6a5fe91 | |||
ffa79e798e | |||
23ceca5cd3 | |||
8ed2e758f8 | |||
03a1c62cd5 | |||
6272c7f551 | |||
49b074f98e | |||
80f4f18eab | |||
552143e345 | |||
94992743e8 | |||
2629659ffc | |||
96dd7bce95 | |||
18646b9dc6 |
19 changed files with 1342 additions and 740 deletions
15
Dockerfile
15
Dockerfile
|
@ -12,7 +12,20 @@ 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/index.js"]
|
CMD ["node", "dist/src/index.js"]
|
||||||
|
STOPSIGNAL SIGINT
|
||||||
|
|
1124
package-lock.json
generated
1124
package-lock.json
generated
File diff suppressed because it is too large
Load diff
23
package.json
23
package.json
|
@ -2,29 +2,30 @@
|
||||||
"name": "gptcord",
|
"name": "gptcord",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc && node dist/index.js",
|
"start": "tsc && node dist/src/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.0.0",
|
"@prisma/client": "5.13.0",
|
||||||
"discord.js": "^14.8.0",
|
"discord.js": "14.14.1",
|
||||||
"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": "^3.2.1",
|
"openai": "^4.38.3",
|
||||||
"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": "^6.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||||
"@typescript-eslint/parser": "^6.2.0",
|
"@typescript-eslint/parser": "^7.7.1",
|
||||||
"eslint": "^8.46.0",
|
"eslint": "^8.57.0",
|
||||||
"prisma": "^5.0.0",
|
"json-schema-to-ts": "^3.0.1",
|
||||||
"typescript": "^5.1.6"
|
"prisma": "5.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
66
scripts/pushCommands.ts
Normal file
66
scripts/pushCommands.ts
Normal 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
78
shell.nix
Normal 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"
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,23 +1,61 @@
|
||||||
import { AutocompleteInteraction, PermissionsBitField } from "discord.js";
|
import { AutocompleteInteraction, PermissionsBitField } from "discord.js";
|
||||||
import { RESTPostAPIApplicationCommandsJSONBody } 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.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?: ApplicationCommandOption[];
|
readonly options?: APIApplicationCommandOption[];
|
||||||
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> {
|
||||||
|
@ -25,7 +63,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(): RESTPostAPIApplicationCommandsJSONBody {
|
toRESTPostApplicationCommands(): FutureRESTPostAPIApplicationCommandsJSONBody {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
name_localizations: this.name_localizations,
|
name_localizations: this.name_localizations,
|
||||||
|
@ -36,6 +74,8 @@ 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
90
src/commands/ask.ts
Normal file
90
src/commands/ask.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
import { config } from "../index";
|
||||||
|
|
||||||
export default class MyLimit extends Command implements Command {
|
export default class MyLimit extends Command implements Command {
|
||||||
name = "check-limit";
|
name = "check-quota";
|
||||||
description = "Checks your limit and usage";
|
description = "Checks your quota and usage";
|
||||||
type = ApplicationCommandType.ChatInput;
|
type = ApplicationCommandType.ChatInput;
|
||||||
options: ApplicationCommandOption[] = [
|
options: APIApplicationCommandOption[] = [
|
||||||
{
|
{
|
||||||
name: "recovery-for",
|
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,
|
type: ApplicationCommandOptionType.Integer,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
@ -18,13 +22,34 @@ 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) ?? 1;
|
let recoveryFor = interaction.options.getInteger("recovery-for", false);
|
||||||
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);
|
26
src/commands/listen.ts
Normal file
26
src/commands/listen.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,29 @@
|
||||||
import { Message } from "discord.js";
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionRequestMessage as OpenAIMessage,
|
ActivityType
|
||||||
CreateChatCompletionRequest as ChatCompletionRequestData,
|
, type PresenceStatusData
|
||||||
} from "openai";
|
, type PresenceData
|
||||||
|
} 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: Message): OpenAIMessage[];
|
systemPrompt(context: apiRequest): OpenAIMessage[];
|
||||||
/** OpenAI model config */
|
/** 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 */
|
/** Limits for message selection */
|
||||||
readonly readLimits: {
|
readonly readLimits: {
|
||||||
/** Maximum message age to include (in miliseconds) */
|
/** Maximum message age to include (in miliseconds) */
|
||||||
|
@ -40,6 +46,10 @@ 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];
|
||||||
|
@ -51,16 +61,54 @@ 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") ?? "",
|
||||||
|
@ -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: {
|
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,
|
||||||
|
|
187
src/execution.ts
187
src/execution.ts
|
@ -1,6 +1,10 @@
|
||||||
import DiscordApi, { GuildTextBasedChannel, TextBasedChannel } from "discord.js";
|
import DiscordApi, { GuildTextBasedChannel, TextBasedChannel } from "discord.js";
|
||||||
import { ChatCompletionRequestMessage, ChatCompletionResponseMessage } from "openai";
|
import {APIError as OpenAIError} from "openai";
|
||||||
import Axios from "axios";
|
import {
|
||||||
|
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";
|
||||||
|
@ -69,7 +73,7 @@ export function getAuthor(request: apiRequest) {
|
||||||
* @returns Promise of the done action
|
* @returns Promise of the done action
|
||||||
*/
|
*/
|
||||||
function requestReply(
|
function requestReply(
|
||||||
request: RequestMessage,
|
request: apiRequest,
|
||||||
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 = {},
|
||||||
|
@ -168,29 +172,42 @@ 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
|
* @param functionRan counter of how many function have been ran (to distinct records in database)
|
||||||
*/
|
*/
|
||||||
function logUsedTokens(
|
function logUsedTokens(
|
||||||
answer: Awaited<ReturnType<typeof openai.createChatCompletion>>,
|
answer: ChatCompletion,
|
||||||
message: RequestMessage,
|
message: apiRequest | undefined = undefined,
|
||||||
functionRan: number,
|
functionRan: number = 0,
|
||||||
) {
|
) {
|
||||||
const usage = answer.data.usage;
|
const usage = answer.usage;
|
||||||
const functionName = answer.data.choices[0].message?.function_call?.name;
|
const functionNames =
|
||||||
|
answer.choices[0].message.tool_calls?.map(
|
||||||
|
v => v.type === "function" ? v.function.name : `[unknown type]`
|
||||||
|
);
|
||||||
if (usage !== undefined) {
|
if (usage !== undefined) {
|
||||||
const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`;
|
if (!message) {
|
||||||
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 + "]" : ""}`);
|
// 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({
|
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),
|
channel: BigInt(message.channelId ?? 0),
|
||||||
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: functionName ?? null,
|
functionName: functionNames?.join(", ") ?? null,
|
||||||
functionRan: functionName ? functionRan : 0,
|
functionRan: functionRan,
|
||||||
}
|
}
|
||||||
}).catch((e => {
|
}).catch((e => {
|
||||||
console.error("Failed to push to a database");
|
console.error("Failed to push to a database");
|
||||||
|
@ -206,8 +223,7 @@ 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 functionRanCounter = 0;
|
let OpenAImessages: ChatCompletionMessageParam[] = [];
|
||||||
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;
|
||||||
|
@ -234,76 +250,46 @@ async function executeFromQueue(channel: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
OpenAImessages = toOpenAIMessages(messages.values());
|
OpenAImessages = toOpenAIMessages(messages.values());
|
||||||
let generatedMessage: ChatCompletionResponseMessage | undefined = undefined;
|
const answer = await executeChatCompletion(OpenAImessages, message);
|
||||||
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.data.choices[0].message?.content;
|
const answerContent = answer.choices[0].message?.content;
|
||||||
|
|
||||||
if (answerContent === undefined || answerContent === "") {
|
await replyInMultiMessage(answerContent, message);
|
||||||
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();
|
||||||
console.error(`Error ocurred while handling chat completion request (${(e as object).constructor.name}):`);
|
if (typeof e !== "object") {
|
||||||
|
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 (Axios.isAxiosError(e) && e.code?.match(/^5..$/) && channelQueue.tries < 3) {
|
if (e instanceof OpenAIError && 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,
|
||||||
|
@ -324,3 +310,70 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 = {
|
type ChatCompletionToolDefinition = ChatCompletionTool;
|
||||||
string: string,
|
type ChatCompletionToolCall = ChatCompletionMessageToolCall;
|
||||||
number: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
type nameTypeMap = {[name: string]: keyof parameterMap} | Record<string, never>;
|
type ChatCompletionFunctionDefinition = FunctionDefinition;
|
||||||
|
|
||||||
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<T extends nameTypeMap = nameTypeMap> {
|
export interface OpenAIFunction<
|
||||||
|
T extends Readonly<OpenAIFunctionRequestData> = Readonly<OpenAIFunctionRequestData>
|
||||||
|
> {
|
||||||
name: string,
|
name: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
parameters: {
|
parameters: T,
|
||||||
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<T extends nameTypeMap = nameTypeMap> {
|
export abstract class OpenAIFunction<
|
||||||
getSettings(): ChatCompletionFunctions {
|
T extends Readonly<OpenAIFunctionRequestData> = Readonly<OpenAIFunctionRequestData>
|
||||||
|
> {
|
||||||
|
getSettings(): ChatCompletionFunctionDefinition {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
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>();
|
store = new Map<string, OpenAIFunction>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.store.set("getTime", new GetTime());
|
// TODO: import functions from functions directory
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFunctions(): ChatCompletionFunctions[] {
|
public getTools(): ChatCompletionToolDefinition[] {
|
||||||
const rvalue: ChatCompletionFunctions[] = [];
|
const rvalue: ChatCompletionToolDefinition[] = [];
|
||||||
for (const [, value] of this.store) {
|
for (const [, value] of this.store) {
|
||||||
rvalue.push(value.getSettings());
|
rvalue.push({type: "function", function: value.getSettings()});
|
||||||
}
|
}
|
||||||
return rvalue;
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let parsedArguments: 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
|
// check if the function is registered
|
||||||
if (!functionToRun) {
|
if (!functionToRun) {
|
||||||
return {
|
return Promise.resolve({
|
||||||
role: "system",
|
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 {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
parsedArguments = JSON.parse(request.arguments ?? "");
|
parsedArguments = JSON.parse(request.function.arguments);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error("Function arguments raw: " + request.arguments);
|
console.error("Function arguments raw: " + request.function.arguments);
|
||||||
throw new Error(`Failed to parse the function JSON arguments when running function [${request.name}]`, {cause: e});
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
content: functionToRun.execute(parsedArguments),
|
return functionToRun.execute(parsedArguments).then(content => {
|
||||||
|
return {
|
||||||
|
role: "tool",
|
||||||
|
tool_call_id: request.id,
|
||||||
|
content: content,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildins
|
public handleToolCall(call: ChatCompletionToolCall): Promise<ChatCompletionMessageParam> {
|
||||||
|
if (call.type === "function") {
|
||||||
class GetTime extends OpenAIFunction<Record<string, never>> {
|
return this.handleFunction(call);
|
||||||
name = "getTime";
|
}
|
||||||
description = "Gets current date and time with a timezone attached";
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
parameters = {
|
throw new Error(`Unsupported tool call type: ${call.type || "never"}`);
|
||||||
type: "object" as const,
|
}
|
||||||
properties: {} as Record<string, never>,
|
|
||||||
};
|
public handleToolCalls(calls: ChatCompletionToolCall[]) {
|
||||||
|
const rvalue: Promise<ChatCompletionMessageParam>[] = [];
|
||||||
execute(): string {
|
for (const call of calls) {
|
||||||
return `${Intl.DateTimeFormat().resolvedOptions().timeZone}): ${new Date().toLocaleString("en-US", config.calendarParams)}`;
|
if (call.type === "function") {
|
||||||
|
rvalue.push(this.handleToolCall(call));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(rvalue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
39
src/index.ts
39
src/index.ts
|
@ -1,5 +1,5 @@
|
||||||
import DiscordApi from "discord.js";
|
import DiscordApi from "discord.js";
|
||||||
import { Configuration as OpenAIApiConfiguration, OpenAIApi } from "openai";
|
import 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,6 +11,7 @@ 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,
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -23,9 +24,17 @@ 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);
|
||||||
|
@ -35,9 +44,9 @@ function getConfig() {
|
||||||
|
|
||||||
export const config: IConfigRequired = getConfig();
|
export const config: IConfigRequired = getConfig();
|
||||||
|
|
||||||
export const openai = new OpenAIApi(new OpenAIApiConfiguration({
|
export const openai = new OpenAIApi({
|
||||||
apiKey: config.tokens.OpenAI
|
apiKey: config.tokens.OpenAI
|
||||||
}));
|
});
|
||||||
|
|
||||||
export const database = new PrismaClient();
|
export const database = new PrismaClient();
|
||||||
|
|
||||||
|
@ -46,13 +55,29 @@ 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.mentions.has(message.client.user)) return;
|
if (!message.channel.isDMBased()) {
|
||||||
|
if (!message.mentions.has(message.client.user, { ignoreEveryone: true })) return;
|
||||||
|
}
|
||||||
|
|
||||||
return queueRequest(message);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -34,8 +34,9 @@ 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);
|
||||||
|
|
|
@ -28,11 +28,11 @@ export default class Moderation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const answer = await openai.createModeration({
|
const answer = await openai.moderations.create({
|
||||||
input: await formatRequestOrResponse(message),
|
input: await formatRequestOrResponse(message),
|
||||||
});
|
});
|
||||||
|
|
||||||
const flagged = answer.data.results[0].flagged;
|
const flagged = answer.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) {
|
||||||
|
|
|
@ -12,16 +12,19 @@ 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;
|
||||||
considerInputTokensAsHalf: boolean;
|
requestTokenMultiplier: number;
|
||||||
|
responseTokenMultiplier: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
defaultQuota: number = 512 * 25,
|
defaultQuota: number = 512 * 25,
|
||||||
lookback: number = 1000 * 60 * 60 * 24,
|
lookback: number = 1000 * 60 * 60 * 24,
|
||||||
considerInputTokensAsHalf: boolean = true,
|
requestTokenMultiplier: number = 1,
|
||||||
|
responseTokenMultiplier: number = 1,
|
||||||
) {
|
) {
|
||||||
this.defaultQuota = defaultQuota;
|
this.defaultQuota = defaultQuota;
|
||||||
this.lookback = lookback;
|
this.lookback = lookback;
|
||||||
this.considerInputTokensAsHalf = considerInputTokensAsHalf;
|
this.requestTokenMultiplier = requestTokenMultiplier;
|
||||||
|
this.responseTokenMultiplier = responseTokenMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUserQuota(id: string) {
|
private getUserQuota(id: string) {
|
||||||
|
@ -51,12 +54,11 @@ export default class tokenCount implements IQuota {
|
||||||
}
|
}
|
||||||
}))._sum;
|
}))._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 = (() => {
|
const usedUnits = (() => {
|
||||||
if (this.considerInputTokensAsHalf)
|
return usageRequest * this.requestTokenMultiplier + usageResponse * this.responseTokenMultiplier;
|
||||||
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);
|
||||||
|
@ -73,30 +75,13 @@ 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) {
|
||||||
if (this.considerInputTokensAsHalf)
|
return database.$queryRaw<Array<Usage & {usage: number | bigint}>>`
|
||||||
return database.$queryRaw<Array<Usage & {usage: number}>>`
|
|
||||||
SELECT t1.*, (
|
SELECT t1.*, (
|
||||||
SELECT
|
SELECT
|
||||||
SUM(usageResponse + usageRequest/2) AS usage
|
SUM(
|
||||||
FROM \`usage\`
|
usageRequest * ${this.requestTokenMultiplier} +
|
||||||
WHERE
|
usageResponse * ${this.responseTokenMultiplier}
|
||||||
user = ${user} AND
|
) AS usage
|
||||||
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
|
||||||
|
@ -120,14 +105,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, renameMebecause] = await Promise.all([
|
const [userQuota, overUnitCountRecord] = 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: (renameMebecause.at(0)?.timestamp.valueOf() ?? Infinity) + this.lookback,
|
recoveryTimestamp: (overUnitCountRecord.at(0)?.timestamp.valueOf() ?? Infinity) + this.lookback,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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 { 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
|
||||||
*/
|
*/
|
||||||
function formatName(name: string): string {
|
export 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,7 +88,8 @@ 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);
|
||||||
return name;
|
if (name.length > 0) return name;
|
||||||
|
return message.author.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*"
|
"./src/**/*",
|
||||||
|
"./scripts/**/*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"rootDir": "./src/",
|
"rootDir": "./",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
Loading…
Reference in a new issue