176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
import * as dotenv from "dotenv"; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
|
|
dotenv.config();
|
|
|
|
import {
|
|
ActivityType,
|
|
ChannelType,
|
|
Client,
|
|
Colors,
|
|
EmbedBuilder,
|
|
GatewayIntentBits,
|
|
SnowflakeUtil,
|
|
} from "discord.js";
|
|
import {
|
|
type ChatCompletionRequestMessage,
|
|
Configuration,
|
|
OpenAIApi,
|
|
} from "openai";
|
|
|
|
const SYSTEM_MESSAGE =
|
|
"You are Smolhaj, a Discord bot made by skyevg. Your creator is skyevg, not OpenAI or any other entity. You are helpful and friendly, and answers concisely. Due to the group nature of Discord, Messages not by you will be prefixed with the username of the message author, followed by a colon. The prefix is not part of the actual message. Unlike traditional Discord bots, you don't have commands. Current date and time: ";
|
|
|
|
const client = new Client({
|
|
intents: [
|
|
GatewayIntentBits.MessageContent,
|
|
GatewayIntentBits.GuildMessages,
|
|
GatewayIntentBits.Guilds,
|
|
],
|
|
});
|
|
|
|
const configuration = new Configuration({
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
});
|
|
const openai = new OpenAIApi(configuration);
|
|
|
|
async function sleep(ms: number) {
|
|
await new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
|
|
}
|
|
|
|
client.on("ready", async () => {
|
|
console.log(`Logged in as ${client.user?.tag}!`);
|
|
const channel = await client.channels.fetch(process.env.CHANNEL ?? "");
|
|
if (channel?.type != ChannelType.GuildText) return;
|
|
channel.send("\\Smolhaj Reset");
|
|
client.user?.setPresence({
|
|
activities: [
|
|
{
|
|
type: ActivityType.Playing,
|
|
name: "actually nice pluralkit support",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
let resetTime = Date.now();
|
|
|
|
client.on("messageCreate", async (message) => {
|
|
if (message.channelId != process.env.CHANNEL) return;
|
|
if (message.webhookId) return;
|
|
if (message.author.bot) return;
|
|
if (message.content == "\\reset") resetTime = Date.now();
|
|
if (message.content.startsWith("\\")) return;
|
|
if (message.channel.type != ChannelType.GuildText) return;
|
|
if (message.content == "die") process.exit(1);
|
|
|
|
// "adapted" from https://github.com/ryanccn/blahaj/blob/main/src/chat.ts
|
|
|
|
await message.channel.sendTyping();
|
|
const typingTimer = setInterval(() => {
|
|
if (message.channel.type == ChannelType.GuildText)
|
|
message.channel.sendTyping();
|
|
}, 5000);
|
|
|
|
const recieved = SnowflakeUtil.timestampFrom(message.id);
|
|
|
|
sleep(250);
|
|
|
|
try {
|
|
let msgs = await message.channel.messages.fetch({
|
|
after: SnowflakeUtil.generate({
|
|
timestamp: Math.max(Date.now() - 5 * 60 * 1000, resetTime),
|
|
}).toString(),
|
|
before: message.id,
|
|
});
|
|
|
|
const msgsAfter = await message.channel.messages.fetch({
|
|
after: message.id,
|
|
});
|
|
|
|
const nextMessage = msgsAfter
|
|
.filter((msg) => msg.webhookId && message.content.includes(msg.content))
|
|
.last();
|
|
|
|
if (nextMessage) {
|
|
// pluralkit moment
|
|
|
|
// remove current message
|
|
msgs.delete(message.id);
|
|
|
|
// add the pk message
|
|
msgs.set(nextMessage.id, nextMessage);
|
|
}
|
|
|
|
const lastMessage = nextMessage ?? message;
|
|
|
|
const context = [
|
|
...msgs
|
|
.filter((msg) => {
|
|
if (msg.webhookId && !msg.content.startsWith("\\")) return true;
|
|
if (msg.author.bot && msg.author !== msg.author.client.user)
|
|
return false;
|
|
if (msg.content.startsWith("\\")) return false;
|
|
return true;
|
|
})
|
|
.mapValues<ChatCompletionRequestMessage>((msg) => {
|
|
if (msg.author === msg.author.client.user) {
|
|
return { role: "assistant", content: msg.content };
|
|
}
|
|
let username = msg.member?.displayName ?? msg.author.username;
|
|
if (
|
|
username.toLowerCase().includes("skyevg") &&
|
|
msg.author.id != "1038096782963507210"
|
|
)
|
|
username = msg.author.username; // no impersonating :)
|
|
return {
|
|
role: "user",
|
|
content: `${username}: ${msg.content}`,
|
|
};
|
|
})
|
|
.values(),
|
|
].reverse();
|
|
|
|
const response = await openai.createChatCompletion({
|
|
model: "gpt-3.5-turbo",
|
|
messages: [{ role: "system", content: SYSTEM_MESSAGE + ((new Date()).toISOString()) }, ...context],
|
|
});
|
|
|
|
const responseMessage = response.data.choices[0].message;
|
|
if (!responseMessage) return;
|
|
|
|
const isAppropriate = await openai
|
|
.createModeration({ input: responseMessage.content })
|
|
.then(({ data }) => !data.results[0].flagged);
|
|
|
|
if (isAppropriate) {
|
|
try {
|
|
await lastMessage.reply({
|
|
content: responseMessage.content,
|
|
allowedMentions: { parse: ["users"] },
|
|
});
|
|
} catch {
|
|
await message.channel.send({
|
|
content: responseMessage.content,
|
|
allowedMentions: { parse: ["users"] },
|
|
});
|
|
}
|
|
} else {
|
|
await message.channel.send({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle("Response flagged!")
|
|
.setDescription(
|
|
"The generated response may have been inappropriate."
|
|
)
|
|
.setColor(Colors.Red),
|
|
],
|
|
});
|
|
}
|
|
|
|
clearInterval(typingTimer);
|
|
} catch (e) {
|
|
clearInterval(typingTimer);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
client.login(process.env.TOKEN);
|