Formatting with Prettier
This commit is contained in:
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
*.js
|
||||
*.d.ts
|
||||
coverage
|
||||
.vscode
|
||||
.idea
|
||||
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# FemScoreboard
|
||||
|
||||
Web application/Discord bot which ranks users based on the reactions received on their messages.
|
||||
Web application/Discord bot which ranks users based on the reactions received on their messages.
|
||||
|
||||
## Project Setup
|
||||
|
||||
|
||||
148
discord/bot.ts
148
discord/bot.ts
@@ -16,9 +16,10 @@ import {
|
||||
MessageReaction,
|
||||
MessageType,
|
||||
PartialMessageReaction,
|
||||
Partials, SlashCommandBuilder,
|
||||
Partials,
|
||||
SlashCommandBuilder,
|
||||
TextChannel,
|
||||
User
|
||||
User,
|
||||
} from 'discord.js';
|
||||
import fs = require('node:fs');
|
||||
import path = require('node:path');
|
||||
@@ -35,25 +36,33 @@ import {
|
||||
requestTTSResponse,
|
||||
serializeMessageHistory,
|
||||
sync,
|
||||
REAL_NAMES
|
||||
REAL_NAMES,
|
||||
} from './util';
|
||||
import 'dotenv/config';
|
||||
import { LLMConfig } from './commands/types';
|
||||
import { LLMProvider } from './provider/provider';
|
||||
|
||||
interface State {
|
||||
llmconf?(): LLMConfig,
|
||||
provider?(): LLMProvider,
|
||||
sysprompt?(): string
|
||||
llmconf?(): LLMConfig;
|
||||
provider?(): LLMProvider;
|
||||
sysprompt?(): string;
|
||||
}
|
||||
const state: State = {};
|
||||
|
||||
interface CommandClient extends Client {
|
||||
commands?: Collection<string, { data: SlashCommandBuilder, execute: (interaction: Interaction) => Promise<void> }>
|
||||
commands?: Collection<
|
||||
string,
|
||||
{ data: SlashCommandBuilder; execute: (interaction: Interaction) => Promise<void> }
|
||||
>;
|
||||
}
|
||||
|
||||
const client: CommandClient = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.MessageContent],
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||
});
|
||||
client.commands = new Collection();
|
||||
@@ -68,8 +77,10 @@ client.once(Events.ClientReady, async () => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function onMessageReactionChanged(reaction: MessageReaction | PartialMessageReaction, user: User) {
|
||||
async function onMessageReactionChanged(
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
user: User
|
||||
) {
|
||||
// When a reaction is received, check if the structure is partial
|
||||
if (reaction.partial) {
|
||||
// If the message this reaction belongs to was removed, the fetching might result in an API error which should be handled
|
||||
@@ -93,13 +104,17 @@ async function onMessageReactionChanged(reaction: MessageReaction | PartialMessa
|
||||
}
|
||||
|
||||
// Now the message has been cached and is fully available
|
||||
logInfo(`[bot] ${reaction.message.author?.id}'s message reaction count changed: ${reaction.emoji.name}x${reaction.count}`);
|
||||
logInfo(
|
||||
`[bot] ${reaction.message.author?.id}'s message reaction count changed: ${reaction.emoji.name}x${reaction.count}`
|
||||
);
|
||||
await recordReaction(<MessageReaction>reaction);
|
||||
}
|
||||
|
||||
function textOnlyMessages(message: Message) {
|
||||
return message.cleanContent.length > 0 &&
|
||||
(message.type === MessageType.Default || message.type === MessageType.Reply);
|
||||
return (
|
||||
message.cleanContent.length > 0 &&
|
||||
(message.type === MessageType.Default || message.type === MessageType.Reply)
|
||||
);
|
||||
}
|
||||
|
||||
function isGoodResponse(response: string) {
|
||||
@@ -118,7 +133,7 @@ async function onNewMessage(message: Message) {
|
||||
const audioBuf = await audio.arrayBuffer();
|
||||
const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName('mikuified.wav');
|
||||
await message.reply({
|
||||
files: [audioFile]
|
||||
files: [audioFile],
|
||||
});
|
||||
} catch (err) {
|
||||
logError(`[bot] Failed to generate audio message reply: ${err}`);
|
||||
@@ -131,18 +146,20 @@ async function onNewMessage(message: Message) {
|
||||
}
|
||||
|
||||
// Miku must reply when spoken to
|
||||
const mustReply = message.mentions.has(process.env.CLIENT!) || message.cleanContent.toLowerCase().includes('miku');
|
||||
const mustReply =
|
||||
message.mentions.has(process.env.CLIENT!) ||
|
||||
message.cleanContent.toLowerCase().includes('miku');
|
||||
|
||||
const history = await message.channel.messages.fetch({
|
||||
limit: state.llmconf!().msg_context - 1,
|
||||
before: message.id
|
||||
before: message.id,
|
||||
});
|
||||
|
||||
// change Miku's message probability depending on current message frequency
|
||||
const historyMessages = [...history.values()].reverse();
|
||||
//const historyTimes = historyMessages.map((m: Message) => m.createdAt.getTime());
|
||||
//const historyAvgDelayMins = (historyTimes[historyTimes.length - 1] - historyTimes[0]) / 60000;
|
||||
const replyChance = Math.floor(Math.random() * 1 / Number(process.env.REPLY_CHANCE)) === 0;
|
||||
const replyChance = Math.floor((Math.random() * 1) / Number(process.env.REPLY_CHANCE)) === 0;
|
||||
const willReply = mustReply || replyChance;
|
||||
|
||||
if (!willReply) {
|
||||
@@ -163,7 +180,11 @@ async function onNewMessage(message: Message) {
|
||||
await message.channel.sendTyping();
|
||||
}
|
||||
|
||||
const response = await state.provider!().requestLLMResponse(cleanHistoryList, state.sysprompt!(), state.llmconf!());
|
||||
const response = await state.provider!().requestLLMResponse(
|
||||
cleanHistoryList,
|
||||
state.sysprompt!(),
|
||||
state.llmconf!()
|
||||
);
|
||||
// evaluate response
|
||||
if (!isGoodResponse(response)) {
|
||||
logWarn(`[bot] Burning bad response: "${response}"`);
|
||||
@@ -198,7 +219,7 @@ async function requestRVCResponse(src: Attachment): Promise<Blob> {
|
||||
logInfo(`[bot] Got audio file: ${srcbuf.size} bytes`);
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("token", process.env.LLM_TOKEN || "");
|
||||
queryParams.append('token', process.env.LLM_TOKEN || '');
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', fs.readFileSync(tmpFileName), 'voice-message.ogg');
|
||||
@@ -207,7 +228,7 @@ async function requestRVCResponse(src: Attachment): Promise<Blob> {
|
||||
logInfo(`[bot] Requesting RVC response for ${src.id}`);
|
||||
const res = await fetch(rvcEndpoint, {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
body: fd,
|
||||
});
|
||||
const resContents = await res.blob();
|
||||
return resContents;
|
||||
@@ -228,10 +249,12 @@ async function scheduleRandomMessage(firstTime = false) {
|
||||
try {
|
||||
const audio = await requestTTSResponse(randomMessage);
|
||||
const audioBuf = await audio.arrayBuffer();
|
||||
const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName('mikuified.wav');
|
||||
const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName(
|
||||
'mikuified.wav'
|
||||
);
|
||||
await channel.send({
|
||||
content: randomMessage,
|
||||
files: [audioFile]
|
||||
files: [audioFile],
|
||||
});
|
||||
logInfo(`[bot] Sent MOTD + TTS: ${randomMessage}`);
|
||||
} catch (err) {
|
||||
@@ -271,7 +294,9 @@ async function scheduleThrowback(firstTime = false) {
|
||||
|
||||
const channel = <TextChannel>await client.channels.fetch(process.env.THROWBACK_CHANNEL);
|
||||
if (!channel) {
|
||||
logWarn(`[bot] Channel ${process.env.THROWBACK_CHANNEL} not found, disabling throwback.`);
|
||||
logWarn(
|
||||
`[bot] Channel ${process.env.THROWBACK_CHANNEL} not found, disabling throwback.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -282,19 +307,22 @@ async function scheduleThrowback(firstTime = false) {
|
||||
|
||||
// Convert to approximate snowflake ID
|
||||
const aroundSnowflake = dateToSnowflake(oneYearAgo);
|
||||
logInfo(`[bot] Fetching messages around ${oneYearAgo.toISOString()} (snowflake: ${aroundSnowflake})`);
|
||||
logInfo(
|
||||
`[bot] Fetching messages around ${oneYearAgo.toISOString()} (snowflake: ${aroundSnowflake})`
|
||||
);
|
||||
|
||||
// Fetch messages around that time
|
||||
const messages = await channel.messages.fetch({
|
||||
around: aroundSnowflake,
|
||||
limit: 50
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
// Filter to only text messages from non-bots
|
||||
const textMessages = messages.filter(m =>
|
||||
!m.author.bot &&
|
||||
m.cleanContent.length > 0 &&
|
||||
(m.type === MessageType.Default || m.type === MessageType.Reply)
|
||||
const textMessages = messages.filter(
|
||||
(m) =>
|
||||
!m.author.bot &&
|
||||
m.cleanContent.length > 0 &&
|
||||
(m.type === MessageType.Default || m.type === MessageType.Reply)
|
||||
);
|
||||
|
||||
if (textMessages.size === 0) {
|
||||
@@ -304,7 +332,9 @@ async function scheduleThrowback(firstTime = false) {
|
||||
const messagesArray = [...textMessages.values()];
|
||||
const randomMsg = messagesArray[Math.floor(Math.random() * messagesArray.length)];
|
||||
|
||||
logInfo(`[bot] Selected throwback message from ${randomMsg.author.username}: "${randomMsg.cleanContent}"`);
|
||||
logInfo(
|
||||
`[bot] Selected throwback message from ${randomMsg.author.username}: "${randomMsg.cleanContent}"`
|
||||
);
|
||||
|
||||
// Generate LLM response using the standard system prompt
|
||||
if ('sendTyping' in channel) {
|
||||
@@ -370,21 +400,30 @@ async function scheduleBiggestLoser(firstTime = false) {
|
||||
let lastId = startId;
|
||||
while (true) {
|
||||
try {
|
||||
const messages = await (textChannel as any).messages.fetch({ after: lastId, limit: 100 });
|
||||
const messages = await (textChannel as any).messages.fetch({
|
||||
after: lastId,
|
||||
limit: 100,
|
||||
});
|
||||
if (messages.size === 0) break;
|
||||
|
||||
|
||||
let maxId = lastId;
|
||||
for (const [msgId, msg] of messages) {
|
||||
if (BigInt(msgId) > BigInt(maxId)) maxId = msgId;
|
||||
if (BigInt(msgId) >= BigInt(endId)) continue;
|
||||
if (!msg.author.bot && (REAL_NAMES as any)[msg.author.username]) {
|
||||
if (
|
||||
!msg.author.bot &&
|
||||
(REAL_NAMES as any)[msg.author.username]
|
||||
) {
|
||||
const realName = (REAL_NAMES as any)[msg.author.username];
|
||||
if (realNameToCount.has(realName)) {
|
||||
realNameToCount.set(realName, realNameToCount.get(realName)! + 1);
|
||||
realNameToCount.set(
|
||||
realName,
|
||||
realNameToCount.get(realName)! + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lastId = maxId;
|
||||
if (BigInt(lastId) >= BigInt(endId) || messages.size < 100) break;
|
||||
} catch (e) {
|
||||
@@ -413,7 +452,9 @@ async function scheduleBiggestLoser(firstTime = false) {
|
||||
if (fs.existsSync(streakFile)) {
|
||||
try {
|
||||
const streakData = JSON.parse(fs.readFileSync(streakFile, 'utf8'));
|
||||
const prevNames = Array.isArray(streakData.names) ? streakData.names : [streakData.name];
|
||||
const prevNames = Array.isArray(streakData.names)
|
||||
? streakData.names
|
||||
: [streakData.name];
|
||||
prevNames.sort();
|
||||
if (JSON.stringify(prevNames) === JSON.stringify(biggestLosers)) {
|
||||
streakCount = streakData.count + 1;
|
||||
@@ -422,16 +463,19 @@ async function scheduleBiggestLoser(firstTime = false) {
|
||||
logWarn(`[bot] Failed to read streak data: ${e}`);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(streakFile, JSON.stringify({ names: biggestLosers, count: streakCount }));
|
||||
fs.writeFileSync(
|
||||
streakFile,
|
||||
JSON.stringify({ names: biggestLosers, count: streakCount })
|
||||
);
|
||||
|
||||
const firstNames = biggestLosers.map(n => n.split(' ')[0]);
|
||||
const firstNames = biggestLosers.map((n) => n.split(' ')[0]);
|
||||
let joinedNames = firstNames[0];
|
||||
if (firstNames.length === 2) {
|
||||
joinedNames = `${firstNames[0]} and ${firstNames[1]}`;
|
||||
} else if (firstNames.length > 2) {
|
||||
joinedNames = `${firstNames.slice(0, -1).join(', ')}, and ${firstNames[firstNames.length - 1]}`;
|
||||
}
|
||||
|
||||
|
||||
const isAre = biggestLosers.length > 1 ? 'are' : 'is';
|
||||
const theyHave = biggestLosers.length > 1 ? 'They have' : 'They have';
|
||||
let declaration = `The biggest loser(s) of yesterday ${isAre} ${joinedNames} with only ${minCount} messages! ${theyHave} been the biggest loser(s) for ${streakCount} day(s) in a row.`;
|
||||
@@ -479,14 +523,14 @@ async function scheduleBiggestLoser(firstTime = false) {
|
||||
setTimeout(scheduleBiggestLoser, timeout);
|
||||
}
|
||||
|
||||
client.on(Events.InteractionCreate, async interaction => {
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
});
|
||||
|
||||
client.on(Events.MessageCreate, onNewMessage);
|
||||
client.on(Events.MessageReactionAdd, onMessageReactionChanged);
|
||||
client.on(Events.MessageReactionRemove, onMessageReactionChanged);
|
||||
client.on(Events.InteractionCreate, async interaction => {
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const client: CommandClient = interaction.client;
|
||||
@@ -502,9 +546,15 @@ client.on(Events.InteractionCreate, async interaction => {
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||
await interaction.followUp({
|
||||
content: 'There was an error while executing this command!',
|
||||
ephemeral: true,
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||
await interaction.reply({
|
||||
content: 'There was an error while executing this command!',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -512,13 +562,13 @@ client.on(Events.InteractionCreate, async interaction => {
|
||||
// startup
|
||||
(async () => {
|
||||
tmp.setGracefulCleanup();
|
||||
logInfo("[db] Opening...");
|
||||
logInfo('[db] Opening...');
|
||||
await openDb();
|
||||
logInfo("[db] Migrating...");
|
||||
logInfo('[db] Migrating...');
|
||||
await db.migrate();
|
||||
logInfo("[db] Ready.");
|
||||
logInfo('[db] Ready.');
|
||||
|
||||
logInfo("[bot] Loading commands...");
|
||||
logInfo('[bot] Loading commands...');
|
||||
const foldersPath = path.join(__dirname, 'commands');
|
||||
const commandFolders = fs.readdirSync(foldersPath, { withFileTypes: true });
|
||||
for (const folder of commandFolders) {
|
||||
@@ -526,7 +576,7 @@ client.on(Events.InteractionCreate, async interaction => {
|
||||
continue;
|
||||
}
|
||||
const commandsPath = path.join(foldersPath, folder.name);
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js'));
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(commandsPath, file);
|
||||
const command = require(filePath);
|
||||
@@ -538,7 +588,7 @@ client.on(Events.InteractionCreate, async interaction => {
|
||||
}
|
||||
}
|
||||
|
||||
logInfo("[bot] Logging in...");
|
||||
logInfo('[bot] Logging in...');
|
||||
await client.login(process.env.TOKEN);
|
||||
if (process.env.ENABLE_MOTD) {
|
||||
await scheduleRandomMessage(true);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder
|
||||
} from 'discord.js';
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { LLMConfig } from '../types';
|
||||
import 'dotenv/config';
|
||||
|
||||
@@ -12,23 +9,26 @@ const config: LLMConfig = {
|
||||
top_p: 0.6,
|
||||
msg_context: 8,
|
||||
frequency_penalty: 0.0,
|
||||
presence_penalty: 0.0
|
||||
presence_penalty: 0.0,
|
||||
};
|
||||
|
||||
async function configCommand(interaction: ChatInputCommandInteraction)
|
||||
{
|
||||
async function configCommand(interaction: ChatInputCommandInteraction) {
|
||||
if (interaction.user.id !== process.env.ADMIN) {
|
||||
await interaction.reply("You are not authorized to change model settings");
|
||||
return;
|
||||
await interaction.reply('You are not authorized to change model settings');
|
||||
return;
|
||||
}
|
||||
|
||||
config.max_new_tokens = interaction.options.getInteger('max_new_tokens') ?? config.max_new_tokens;
|
||||
config.min_new_tokens = interaction.options.getInteger('min_new_tokens') ?? config.min_new_tokens;
|
||||
config.max_new_tokens =
|
||||
interaction.options.getInteger('max_new_tokens') ?? config.max_new_tokens;
|
||||
config.min_new_tokens =
|
||||
interaction.options.getInteger('min_new_tokens') ?? config.min_new_tokens;
|
||||
config.msg_context = interaction.options.getInteger('msg_context') ?? config.msg_context;
|
||||
config.temperature = interaction.options.getNumber('temperature') ?? config.temperature;
|
||||
config.top_p = interaction.options.getNumber('top_p') ?? config.top_p;
|
||||
config.frequency_penalty = interaction.options.getNumber('frequency_penalty') ?? config.frequency_penalty;
|
||||
config.presence_penalty = interaction.options.getNumber('presence_penalty') ?? config.presence_penalty;
|
||||
config.frequency_penalty =
|
||||
interaction.options.getNumber('frequency_penalty') ?? config.frequency_penalty;
|
||||
config.presence_penalty =
|
||||
interaction.options.getNumber('presence_penalty') ?? config.presence_penalty;
|
||||
await interaction.reply(`
|
||||
\`\`\`
|
||||
max_new_tokens = ${config.max_new_tokens}
|
||||
@@ -46,26 +46,40 @@ export = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('llmconf')
|
||||
.setDescription('Change model inference settings')
|
||||
.addNumberOption(
|
||||
opt => opt.setName('temperature').setDescription('Temperature; not recommended w/ top_p (default: 0.7)')
|
||||
.addNumberOption((opt) =>
|
||||
opt
|
||||
.setName('temperature')
|
||||
.setDescription('Temperature; not recommended w/ top_p (default: 0.7)')
|
||||
)
|
||||
.addNumberOption(
|
||||
opt => opt.setName('top_p').setDescription('Cumulative prob. of min. token set to sample from; not recommended w/ temperature (default: 0.9)')
|
||||
.addNumberOption((opt) =>
|
||||
opt
|
||||
.setName('top_p')
|
||||
.setDescription(
|
||||
'Cumulative prob. of min. token set to sample from; not recommended w/ temperature (default: 0.9)'
|
||||
)
|
||||
)
|
||||
.addNumberOption(
|
||||
opt => opt.setName('frequency_penalty').setDescription('[unused] Penalize tokens from reappearing multiple times; ranges from -2 to 2 (default: 0.0)')
|
||||
.addNumberOption((opt) =>
|
||||
opt
|
||||
.setName('frequency_penalty')
|
||||
.setDescription(
|
||||
'[unused] Penalize tokens from reappearing multiple times; ranges from -2 to 2 (default: 0.0)'
|
||||
)
|
||||
)
|
||||
.addNumberOption(
|
||||
opt => opt.setName('presence_penalty').setDescription('[unused] Penalize a token from reappearing; ranges from -2 to 2 (default: 0.0)')
|
||||
.addNumberOption((opt) =>
|
||||
opt
|
||||
.setName('presence_penalty')
|
||||
.setDescription(
|
||||
'[unused] Penalize a token from reappearing; ranges from -2 to 2 (default: 0.0)'
|
||||
)
|
||||
)
|
||||
.addIntegerOption(
|
||||
opt => opt.setName('max_new_tokens').setDescription('Max. new tokens (default: 100)')
|
||||
.addIntegerOption((opt) =>
|
||||
opt.setName('max_new_tokens').setDescription('Max. new tokens (default: 100)')
|
||||
)
|
||||
.addIntegerOption(
|
||||
opt => opt.setName('min_new_tokens').setDescription('Min. new tokens (default: 1)')
|
||||
.addIntegerOption((opt) =>
|
||||
opt.setName('min_new_tokens').setDescription('Min. new tokens (default: 1)')
|
||||
)
|
||||
.addIntegerOption(
|
||||
opt => opt.setName('msg_context').setDescription('Num. messages in context (default: 8)')
|
||||
.addIntegerOption((opt) =>
|
||||
opt.setName('msg_context').setDescription('Num. messages in context (default: 8)')
|
||||
),
|
||||
execute: configCommand,
|
||||
state: () => config,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder
|
||||
} from 'discord.js';
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import 'dotenv/config';
|
||||
import fs = require('node:fs');
|
||||
import path = require('node:path');
|
||||
@@ -9,11 +6,10 @@ import path = require('node:path');
|
||||
const syspromptCache = path.resolve(__dirname, 'sysprompt_cache');
|
||||
const SAFE_NAME_REGEX = /^[\w\d]+$/;
|
||||
|
||||
async function editSyspromptCommand(interaction: ChatInputCommandInteraction)
|
||||
{
|
||||
async function editSyspromptCommand(interaction: ChatInputCommandInteraction) {
|
||||
if (interaction.user.id !== process.env.ADMIN) {
|
||||
await interaction.reply("You are not authorized to change model settings");
|
||||
return;
|
||||
await interaction.reply('You are not authorized to change model settings');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('name', true);
|
||||
@@ -38,11 +34,14 @@ export = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('edit')
|
||||
.setDescription('Edit system prompts')
|
||||
.addStringOption(
|
||||
opt => opt.setName('name').setDescription('Name (must be alphanumeric)').setRequired(true)
|
||||
.addStringOption((opt) =>
|
||||
opt.setName('name').setDescription('Name (must be alphanumeric)').setRequired(true)
|
||||
)
|
||||
.addAttachmentOption(
|
||||
opt => opt.setName('content').setDescription('Text file containing the system prompt').setRequired(true)
|
||||
.addAttachmentOption((opt) =>
|
||||
opt
|
||||
.setName('content')
|
||||
.setDescription('Text file containing the system prompt')
|
||||
.setRequired(true)
|
||||
),
|
||||
execute: editSyspromptCommand
|
||||
execute: editSyspromptCommand,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder
|
||||
} from 'discord.js';
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import 'dotenv/config';
|
||||
import { MikuAIProvider } from '../../provider/mikuai';
|
||||
import { HuggingfaceProvider } from '../../provider/huggingface';
|
||||
@@ -12,13 +9,13 @@ const PROVIDERS = {
|
||||
mikuai: new MikuAIProvider(),
|
||||
huggingface: new HuggingfaceProvider(),
|
||||
openai: new OpenAIProvider(),
|
||||
ollama: new OllamaProvider()
|
||||
ollama: new OllamaProvider(),
|
||||
};
|
||||
let provider = PROVIDERS.openai;
|
||||
|
||||
async function providerCommand(interaction: ChatInputCommandInteraction) {
|
||||
if (interaction.user.id !== process.env.ADMIN) {
|
||||
await interaction.reply("You are not authorized to change model settings");
|
||||
await interaction.reply('You are not authorized to change model settings');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,22 +35,19 @@ export = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('provider')
|
||||
.setDescription('Change model backend')
|
||||
.addStringOption(
|
||||
opt => opt.setName('name')
|
||||
.addStringOption((opt) =>
|
||||
opt
|
||||
.setName('name')
|
||||
.setDescription('Name of model backend')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
...Object.keys(PROVIDERS)
|
||||
.map(key => ({
|
||||
name: PROVIDERS[key].name(),
|
||||
value: key
|
||||
}))
|
||||
...Object.keys(PROVIDERS).map((key) => ({
|
||||
name: PROVIDERS[key].name(),
|
||||
value: key,
|
||||
}))
|
||||
)
|
||||
)
|
||||
.addStringOption(
|
||||
opt => opt.setName('model')
|
||||
.setDescription('Model ID')
|
||||
),
|
||||
.addStringOption((opt) => opt.setName('model').setDescription('Model ID')),
|
||||
execute: providerCommand,
|
||||
state: () => provider
|
||||
state: () => provider,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AttachmentBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder
|
||||
} from 'discord.js';
|
||||
import { AttachmentBuilder, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import 'dotenv/config';
|
||||
import fs = require('node:fs');
|
||||
import path = require('node:path');
|
||||
@@ -11,9 +7,7 @@ import { globSync } from 'glob';
|
||||
const syspromptCache = path.resolve(__dirname, 'sysprompt_cache');
|
||||
let sysprompt = fs.readFileSync(path.resolve(syspromptCache, 'nous.txt'), 'utf-8');
|
||||
|
||||
|
||||
function removeTrailingNewlines(sysprompt: string)
|
||||
{
|
||||
function removeTrailingNewlines(sysprompt: string) {
|
||||
// remove trailing '\n' or '\r\n' that editors like to insert
|
||||
if (sysprompt[sysprompt.length - 1] == '\n') {
|
||||
if (sysprompt[sysprompt.length - 2] == '\r') {
|
||||
@@ -24,8 +18,7 @@ function removeTrailingNewlines(sysprompt: string)
|
||||
return sysprompt;
|
||||
}
|
||||
|
||||
function getSysPrompts()
|
||||
{
|
||||
function getSysPrompts() {
|
||||
const absolutePaths = globSync(path.resolve(syspromptCache, '*.txt'));
|
||||
const prompts = {};
|
||||
for (const filepath of absolutePaths) {
|
||||
@@ -36,11 +29,10 @@ function getSysPrompts()
|
||||
return prompts;
|
||||
}
|
||||
|
||||
async function syspromptCommand(interaction: ChatInputCommandInteraction)
|
||||
{
|
||||
async function syspromptCommand(interaction: ChatInputCommandInteraction) {
|
||||
if (interaction.user.id !== process.env.ADMIN) {
|
||||
await interaction.reply("You are not authorized to change model settings");
|
||||
return;
|
||||
await interaction.reply('You are not authorized to change model settings');
|
||||
return;
|
||||
}
|
||||
|
||||
const promptDict = getSysPrompts();
|
||||
@@ -49,17 +41,23 @@ async function syspromptCommand(interaction: ChatInputCommandInteraction)
|
||||
sysprompt = promptDict[chosenPrompt];
|
||||
await interaction.reply({
|
||||
content: `Current system prompt: \`${chosenPrompt}\``,
|
||||
files: [new AttachmentBuilder(Buffer.from(sysprompt), {
|
||||
name: `${chosenPrompt}.txt`
|
||||
})]
|
||||
files: [
|
||||
new AttachmentBuilder(Buffer.from(sysprompt), {
|
||||
name: `${chosenPrompt}.txt`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const warning = chosenPrompt ? `System prompt \`${chosenPrompt}\` not found!` : 'A new system prompt was not specified.';
|
||||
const warning = chosenPrompt
|
||||
? `System prompt \`${chosenPrompt}\` not found!`
|
||||
: 'A new system prompt was not specified.';
|
||||
await interaction.reply({
|
||||
content: `${warning}\nCurrent system prompt:`,
|
||||
files: [new AttachmentBuilder(Buffer.from(sysprompt), {
|
||||
name: 'unknown.txt'
|
||||
})]
|
||||
files: [
|
||||
new AttachmentBuilder(Buffer.from(sysprompt), {
|
||||
name: 'unknown.txt',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -68,9 +66,7 @@ export = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('sysprompt')
|
||||
.setDescription('Get/set the system prompt being used')
|
||||
.addStringOption(
|
||||
opt => opt.setName('name').setDescription('Name of system prompt')
|
||||
),
|
||||
.addStringOption((opt) => opt.setName('name').setDescription('Name of system prompt')),
|
||||
execute: syspromptCommand,
|
||||
state: () => removeTrailingNewlines(sysprompt)
|
||||
state: () => removeTrailingNewlines(sysprompt),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AttachmentBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder
|
||||
} from 'discord.js';
|
||||
import { AttachmentBuilder, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import 'dotenv/config';
|
||||
import { logError } from '../../../logging';
|
||||
import { requestTTSResponse } from '../../util';
|
||||
@@ -10,23 +6,22 @@ import { requestTTSResponse } from '../../util';
|
||||
const config = {
|
||||
ttsSettings: {
|
||||
pitch_change_oct: 1,
|
||||
pitch_change_sem: 0
|
||||
}
|
||||
pitch_change_sem: 0,
|
||||
},
|
||||
};
|
||||
|
||||
async function ttsCommand(interaction: ChatInputCommandInteraction)
|
||||
{
|
||||
async function ttsCommand(interaction: ChatInputCommandInteraction) {
|
||||
const text = interaction.options.getString('text');
|
||||
await interaction.reply(`generating audio for "${text}"...`);
|
||||
try {
|
||||
const audio = await requestTTSResponse(text);
|
||||
const audioBuf = await audio.arrayBuffer();
|
||||
const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName('mikuified.wav');
|
||||
await interaction.editReply({
|
||||
files: [audioFile]
|
||||
});
|
||||
const audio = await requestTTSResponse(text);
|
||||
const audioBuf = await audio.arrayBuffer();
|
||||
const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName('mikuified.wav');
|
||||
await interaction.editReply({
|
||||
files: [audioFile],
|
||||
});
|
||||
} catch (err) {
|
||||
await interaction.editReply(`Error: ${err}`);
|
||||
await interaction.editReply(`Error: ${err}`);
|
||||
logError(`Error while generating TTS: ${err}`);
|
||||
}
|
||||
}
|
||||
@@ -34,10 +29,8 @@ async function ttsCommand(interaction: ChatInputCommandInteraction)
|
||||
export = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('tts')
|
||||
.setDescription('Read text in Miku\'s voice')
|
||||
.addStringOption(
|
||||
opt => opt.setName('text').setDescription('Text').setRequired(true)
|
||||
),
|
||||
.setDescription("Read text in Miku's voice")
|
||||
.addStringOption((opt) => opt.setName('text').setDescription('Text').setRequired(true)),
|
||||
execute: ttsCommand,
|
||||
config: config
|
||||
config: config,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export interface LLMConfig {
|
||||
max_new_tokens: number,
|
||||
min_new_tokens: number,
|
||||
temperature: number,
|
||||
top_p: number,
|
||||
frequency_penalty: number,
|
||||
presence_penalty: number,
|
||||
msg_context: number
|
||||
max_new_tokens: number;
|
||||
min_new_tokens: number;
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
msg_context: number;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ for (const folder of commandFolders) {
|
||||
|
||||
// Grab all the command files from the commands directory you created earlier
|
||||
const commandsPath = path.join(foldersPath, folder.name);
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js'));
|
||||
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(commandsPath, file);
|
||||
@@ -34,9 +34,8 @@ const rest = new REST().setToken(process.env.TOKEN);
|
||||
console.log(`Started refreshing ${commands.length} application (/) commands.`);
|
||||
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
const data = <string[]> await rest.put(
|
||||
Routes.applicationCommands(process.env.CLIENT),
|
||||
{ body: commands },
|
||||
const data = <string[]>(
|
||||
await rest.put(Routes.applicationCommands(process.env.CLIENT), { body: commands })
|
||||
);
|
||||
|
||||
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
|
||||
|
||||
5797
discord/package-lock.json
generated
5797
discord/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,33 @@
|
||||
{
|
||||
"name": "femscoreboardbot",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@huggingface/inference": "^3.1.3",
|
||||
"discord.js": "^14.13.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"emoji-unicode-map": "^1.1.11",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^11.0.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"modelfusion": "^0.135.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ollama": "^0.5.12",
|
||||
"openai": "^6.25.0",
|
||||
"sqlite": "^5.0.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"tmp": "^0.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "npx tsc && node bot.js",
|
||||
"sync": "npx tsc && node sync.js",
|
||||
"deploy": "npx tsc && node deploy.js"
|
||||
}
|
||||
"name": "femscoreboardbot",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@huggingface/inference": "^3.1.3",
|
||||
"discord.js": "^14.13.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"emoji-unicode-map": "^1.1.11",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^11.0.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"modelfusion": "^0.135.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ollama": "^0.5.12",
|
||||
"openai": "^6.25.0",
|
||||
"sqlite": "^5.0.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"tmp": "^0.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "prettier --write .",
|
||||
"build": "tsc",
|
||||
"start": "npm run build && node bot.js",
|
||||
"sync": "npm run build && node sync.js",
|
||||
"deploy": "npm run build && node deploy.js",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import { Message } from 'discord.js';
|
||||
import { LLMProvider } from './provider';
|
||||
import { HfInference } from "@huggingface/inference"
|
||||
import { HfInference } from '@huggingface/inference';
|
||||
import 'dotenv/config';
|
||||
import { serializeMessageHistory } from '../util';
|
||||
import { logError, logInfo } from '../../logging';
|
||||
import { LLMConfig } from '../commands/types';
|
||||
|
||||
|
||||
const RESPONSE_REGEX = `\\{"timestamp":"(Sun|Mon|Tue|Wed|Thu|Fri|Sat), \\d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} \\d{2}:\\d{2}:\\d{2} GMT","author":"Hatsune Miku#1740","name":"Hatsune Miku","context":"([^"\\\\]|\\\\.)*","content":"([^"\\\\]|\\\\.)*"(,"reactions":("(:\\w+: \\(\\d+\\)(, )?)*"|null))?\\}`;
|
||||
|
||||
const RESPONSE_SCHEMA = {
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"description": "When the message was sent, in RFC 7231 format",
|
||||
"title": "Timestamp",
|
||||
"type": "string"
|
||||
properties: {
|
||||
timestamp: {
|
||||
description: 'When the message was sent, in RFC 7231 format',
|
||||
title: 'Timestamp',
|
||||
type: 'string',
|
||||
},
|
||||
"author": {
|
||||
"description": "The author's username, which may be one of the following, or something else: \"vinso1445\", \"f0oby\", \"1thinker\", \"scoliono\", \"ahjc\", \"cinnaba\", \"M6481\", \"hypadrive\", \"need_correction\", \"Hatsune Miku#1740\" (You)",
|
||||
"title": "Author",
|
||||
"type": "string"
|
||||
author: {
|
||||
description:
|
||||
'The author\'s username, which may be one of the following, or something else: "vinso1445", "f0oby", "1thinker", "scoliono", "ahjc", "cinnaba", "M6481", "hypadrive", "need_correction", "Hatsune Miku#1740" (You)',
|
||||
title: 'Author',
|
||||
type: 'string',
|
||||
},
|
||||
"name": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "The author's real name, which may be blank or one of the following: \"Vincent Iannelli\", \"Myles Linden\", \"Samuel Habib\", \"James Shiffer\", \"Alex\", \"Jinsung Park\", \"Lawrence Liu\", \"Nazar Khan\", \"Ethan Cheng\", \"Hatsune Miku\" (You)",
|
||||
"title": "Name"
|
||||
name: {
|
||||
anyOf: [{ type: 'string' }, { type: 'null' }],
|
||||
description:
|
||||
'The author\'s real name, which may be blank or one of the following: "Vincent Iannelli", "Myles Linden", "Samuel Habib", "James Shiffer", "Alex", "Jinsung Park", "Lawrence Liu", "Nazar Khan", "Ethan Cheng", "Hatsune Miku" (You)',
|
||||
title: 'Name',
|
||||
},
|
||||
"context": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"default": null,
|
||||
"description": "The contents of the message being replied to, if this message is a reply",
|
||||
"title": "Context"
|
||||
context: {
|
||||
anyOf: [{ type: 'string' }, { type: 'null' }],
|
||||
default: null,
|
||||
description: 'The contents of the message being replied to, if this message is a reply',
|
||||
title: 'Context',
|
||||
},
|
||||
"content": {
|
||||
"description": "The text content of this message",
|
||||
"title": "Content",
|
||||
"type": "string"
|
||||
content: {
|
||||
description: 'The text content of this message',
|
||||
title: 'Content',
|
||||
type: 'string',
|
||||
},
|
||||
reactions: {
|
||||
anyOf: [{ type: 'string' }, { type: 'null' }],
|
||||
default: null,
|
||||
description:
|
||||
'Optional list of emoji reactions this message received, if any. The following comma-separated format is used: ":skull: (3), :100: (1)"',
|
||||
title: 'Reactions',
|
||||
},
|
||||
"reactions": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional list of emoji reactions this message received, if any. The following comma-separated format is used: \":skull: (3), :100: (1)\"",
|
||||
"title": "Reactions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timestamp",
|
||||
"author",
|
||||
"name",
|
||||
"content"
|
||||
]
|
||||
required: ['timestamp', 'author', 'name', 'content'],
|
||||
};
|
||||
|
||||
const USER_PROMPT = `Continue the following Discord conversation by completing the next message, playing the role of Hatsune Miku. The conversation must progress forward, and you must avoid repeating yourself.
|
||||
@@ -69,16 +57,18 @@ The conversation is as follows. The last line is the message you have to complet
|
||||
|
||||
`;
|
||||
|
||||
|
||||
export class HuggingfaceProvider implements LLMProvider
|
||||
{
|
||||
export class HuggingfaceProvider implements LLMProvider {
|
||||
private client: HfInference;
|
||||
private model: string;
|
||||
|
||||
constructor(hf_token: string | undefined = process.env.HF_TOKEN, model = "NousResearch/Hermes-3-Llama-3.1-8B")
|
||||
{
|
||||
constructor(
|
||||
hf_token: string | undefined = process.env.HF_TOKEN,
|
||||
model = 'NousResearch/Hermes-3-Llama-3.1-8B'
|
||||
) {
|
||||
if (!hf_token) {
|
||||
throw new TypeError("Huggingface API token was not passed in, and environment variable HF_TOKEN was unset!");
|
||||
throw new TypeError(
|
||||
'Huggingface API token was not passed in, and environment variable HF_TOKEN was unset!'
|
||||
);
|
||||
}
|
||||
this.client = new HfInference(hf_token);
|
||||
this.model = model;
|
||||
@@ -92,15 +82,16 @@ export class HuggingfaceProvider implements LLMProvider
|
||||
this.model = id;
|
||||
}
|
||||
|
||||
async requestLLMResponse(history: Message[], sysprompt: string, params: LLMConfig): Promise<string>
|
||||
{
|
||||
let messageList = await Promise.all(
|
||||
history.map(serializeMessageHistory)
|
||||
);
|
||||
messageList = messageList.filter(x => !!x);
|
||||
async requestLLMResponse(
|
||||
history: Message[],
|
||||
sysprompt: string,
|
||||
params: LLMConfig
|
||||
): Promise<string> {
|
||||
let messageList = await Promise.all(history.map(serializeMessageHistory));
|
||||
messageList = messageList.filter((x) => !!x);
|
||||
|
||||
if (messageList.length === 0) {
|
||||
throw new TypeError("No messages with content provided in history!");
|
||||
throw new TypeError('No messages with content provided in history!');
|
||||
}
|
||||
|
||||
// dummy message for last line of prompt
|
||||
@@ -112,21 +103,22 @@ export class HuggingfaceProvider implements LLMProvider
|
||||
|
||||
let templateMsgTxt = JSON.stringify({
|
||||
timestamp: newDate.toUTCString(),
|
||||
author: "Hatsune Miku",
|
||||
name: "Hatsune Miku",
|
||||
author: 'Hatsune Miku',
|
||||
name: 'Hatsune Miku',
|
||||
context: lastMsg!.content,
|
||||
content: "..."
|
||||
content: '...',
|
||||
});
|
||||
|
||||
const messageHistoryTxt = messageList.map(msg => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
|
||||
const messageHistoryTxt =
|
||||
messageList.map((msg) => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
|
||||
logInfo(`[hf] Requesting response for message history: ${messageHistoryTxt}`);
|
||||
|
||||
try {
|
||||
const chatCompletion = await this.client.chatCompletion({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: "system", content: sysprompt },
|
||||
{ role: "user", content: USER_PROMPT + messageHistoryTxt }
|
||||
{ role: 'system', content: sysprompt },
|
||||
{ role: 'user', content: USER_PROMPT + messageHistoryTxt },
|
||||
],
|
||||
temperature: params?.temperature || 0.5,
|
||||
top_p: params?.top_p || 0.9,
|
||||
@@ -141,7 +133,7 @@ export class HuggingfaceProvider implements LLMProvider
|
||||
logInfo(`[hf] API response: ${response}`);
|
||||
|
||||
if (!response) {
|
||||
throw new TypeError("HuggingFace completion API returned no message.");
|
||||
throw new TypeError('HuggingFace completion API returned no message.');
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -5,14 +5,14 @@ import 'dotenv/config';
|
||||
import { logInfo } from '../../logging';
|
||||
import { LLMConfig } from '../commands/types';
|
||||
|
||||
export class MikuAIProvider implements LLMProvider
|
||||
{
|
||||
export class MikuAIProvider implements LLMProvider {
|
||||
private llmToken: string;
|
||||
|
||||
constructor(llmToken: string | undefined = process.env.LLM_TOKEN)
|
||||
{
|
||||
constructor(llmToken: string | undefined = process.env.LLM_TOKEN) {
|
||||
if (!llmToken) {
|
||||
throw new TypeError("LLM token was not passed in, and environment variable LLM_TOKEN was unset!");
|
||||
throw new TypeError(
|
||||
'LLM token was not passed in, and environment variable LLM_TOKEN was unset!'
|
||||
);
|
||||
}
|
||||
this.llmToken = llmToken;
|
||||
}
|
||||
@@ -25,29 +25,32 @@ export class MikuAIProvider implements LLMProvider
|
||||
throw new TypeError('setModel() not implemented on MikuAIProvider.');
|
||||
}
|
||||
|
||||
async requestLLMResponse(history: Message[], sysprompt: string, params: LLMConfig): Promise<string>
|
||||
{
|
||||
async requestLLMResponse(
|
||||
history: Message[],
|
||||
sysprompt: string,
|
||||
params: LLMConfig
|
||||
): Promise<string> {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("token", this.llmToken);
|
||||
queryParams.append("sys_prompt", sysprompt);
|
||||
queryParams.append('token', this.llmToken);
|
||||
queryParams.append('sys_prompt', sysprompt);
|
||||
if (params) {
|
||||
for (const field of Object.keys(params)) {
|
||||
queryParams.append(field, params[field]);
|
||||
}
|
||||
}
|
||||
const llmEndpoint = `${process.env.LLM_HOST}/?${queryParams.toString()}`;
|
||||
let messageList = await Promise.all(
|
||||
history.map(serializeMessageHistory)
|
||||
);
|
||||
messageList = messageList.filter(x => !!x);
|
||||
let messageList = await Promise.all(history.map(serializeMessageHistory));
|
||||
messageList = messageList.filter((x) => !!x);
|
||||
|
||||
logInfo("[bot] Requesting LLM response with message list: " + messageList.map(m => m?.content));
|
||||
logInfo(
|
||||
'[bot] Requesting LLM response with message list: ' + messageList.map((m) => m?.content)
|
||||
);
|
||||
const res = await fetch(llmEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(messageList)
|
||||
body: JSON.stringify(messageList),
|
||||
});
|
||||
const botMsgTxt = await res.text();
|
||||
logInfo(`[bot] Server returned LLM response: ${botMsgTxt}`);
|
||||
|
||||
@@ -6,7 +6,6 @@ import { logError, logInfo } from '../../logging';
|
||||
import { LLMConfig } from '../commands/types';
|
||||
import { Ollama } from 'ollama';
|
||||
|
||||
|
||||
const USER_PROMPT = `Continue the following Discord conversation by completing the next message, playing the role of Hatsune Miku. The conversation must progress forward, and you must avoid repeating yourself.
|
||||
|
||||
Each message is represented as a line of JSON. Refer to other users by their "name" instead of their "author" field whenever possible.
|
||||
@@ -15,16 +14,18 @@ The conversation is as follows. The last line is the message you have to complet
|
||||
|
||||
`;
|
||||
|
||||
|
||||
export class OllamaProvider implements LLMProvider
|
||||
{
|
||||
export class OllamaProvider implements LLMProvider {
|
||||
private client: Ollama;
|
||||
private model: string;
|
||||
|
||||
constructor(host: string | undefined = process.env.LLM_HOST, model = "socialnetwooky/hermes3-llama3.1-abliterated:8b-q5_k_m-64k")
|
||||
{
|
||||
constructor(
|
||||
host: string | undefined = process.env.LLM_HOST,
|
||||
model = 'socialnetwooky/hermes3-llama3.1-abliterated:8b-q5_k_m-64k'
|
||||
) {
|
||||
if (!host) {
|
||||
throw new TypeError("Ollama host was not passed in, and environment variable LLM_HOST was unset!");
|
||||
throw new TypeError(
|
||||
'Ollama host was not passed in, and environment variable LLM_HOST was unset!'
|
||||
);
|
||||
}
|
||||
this.client = new Ollama({ host });
|
||||
this.model = model;
|
||||
@@ -38,15 +39,16 @@ export class OllamaProvider implements LLMProvider
|
||||
this.model = id;
|
||||
}
|
||||
|
||||
async requestLLMResponse(history: Message[], sysprompt: string, params: LLMConfig): Promise<string>
|
||||
{
|
||||
let messageList = await Promise.all(
|
||||
history.map(serializeMessageHistory)
|
||||
);
|
||||
messageList = messageList.filter(x => !!x);
|
||||
async requestLLMResponse(
|
||||
history: Message[],
|
||||
sysprompt: string,
|
||||
params: LLMConfig
|
||||
): Promise<string> {
|
||||
let messageList = await Promise.all(history.map(serializeMessageHistory));
|
||||
messageList = messageList.filter((x) => !!x);
|
||||
|
||||
if (messageList.length === 0) {
|
||||
throw new TypeError("No messages with content provided in history!");
|
||||
throw new TypeError('No messages with content provided in history!');
|
||||
}
|
||||
|
||||
// dummy message for last line of prompt
|
||||
@@ -58,34 +60,35 @@ export class OllamaProvider implements LLMProvider
|
||||
|
||||
let templateMsgTxt = JSON.stringify({
|
||||
timestamp: newDate.toUTCString(),
|
||||
author: "Hatsune Miku",
|
||||
name: "Hatsune Miku",
|
||||
author: 'Hatsune Miku',
|
||||
name: 'Hatsune Miku',
|
||||
context: lastMsg!.content,
|
||||
content: "..."
|
||||
content: '...',
|
||||
});
|
||||
|
||||
const messageHistoryTxt = messageList.map(msg => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
|
||||
const messageHistoryTxt =
|
||||
messageList.map((msg) => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
|
||||
logInfo(`[ollama] Requesting response for message history: ${messageHistoryTxt}`);
|
||||
|
||||
try {
|
||||
const chatCompletion = await this.client.chat({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: "system", content: sysprompt },
|
||||
{ role: "user", content: USER_PROMPT + messageHistoryTxt }
|
||||
{ role: 'system', content: sysprompt },
|
||||
{ role: 'user', content: USER_PROMPT + messageHistoryTxt },
|
||||
],
|
||||
options: {
|
||||
temperature: params?.temperature || 0.5,
|
||||
top_p: params?.top_p || 0.9,
|
||||
num_predict: params?.max_new_tokens || 128,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let response = chatCompletion.message.content;
|
||||
logInfo(`[ollama] API response: ${response}`);
|
||||
|
||||
if (!response) {
|
||||
throw new TypeError("Ollama chat API returned no message.");
|
||||
throw new TypeError('Ollama chat API returned no message.');
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -18,15 +18,20 @@ export class OpenAIProvider implements LLMProvider {
|
||||
private client: OpenAI;
|
||||
private model: string;
|
||||
|
||||
constructor(token: string | undefined = process.env.LLM_TOKEN, model = "zai-org/glm-4.7-flash") {
|
||||
constructor(
|
||||
token: string | undefined = process.env.LLM_TOKEN,
|
||||
model = 'zai-org/glm-4.7-flash'
|
||||
) {
|
||||
if (!token) {
|
||||
throw new TypeError("LLM token was not passed in, and environment variable LLM_TOKEN was unset!");
|
||||
throw new TypeError(
|
||||
'LLM token was not passed in, and environment variable LLM_TOKEN was unset!'
|
||||
);
|
||||
}
|
||||
this.client = new OpenAI({
|
||||
baseURL: process.env.OPENAI_HOST,
|
||||
apiKey: token,
|
||||
apiKey: token,
|
||||
});
|
||||
this.model = model;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
name() {
|
||||
@@ -34,17 +39,19 @@ export class OpenAIProvider implements LLMProvider {
|
||||
}
|
||||
|
||||
setModel(model: string) {
|
||||
this.model = model;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
async requestLLMResponse(history: Message[], sysprompt: string, params: LLMConfig): Promise<string> {
|
||||
let messageList = await Promise.all(
|
||||
history.map(serializeMessageHistory)
|
||||
);
|
||||
messageList = messageList.filter(x => !!x);
|
||||
async requestLLMResponse(
|
||||
history: Message[],
|
||||
sysprompt: string,
|
||||
params: LLMConfig
|
||||
): Promise<string> {
|
||||
let messageList = await Promise.all(history.map(serializeMessageHistory));
|
||||
messageList = messageList.filter((x) => !!x);
|
||||
|
||||
if (messageList.length === 0) {
|
||||
throw new TypeError("No messages with content provided in history!");
|
||||
throw new TypeError('No messages with content provided in history!');
|
||||
}
|
||||
|
||||
// dummy message for last line of prompt
|
||||
@@ -56,35 +63,36 @@ export class OpenAIProvider implements LLMProvider {
|
||||
|
||||
let templateMsgTxt = JSON.stringify({
|
||||
timestamp: newDate.toUTCString(),
|
||||
author: "Hatsune Miku",
|
||||
name: "Hatsune Miku",
|
||||
author: 'Hatsune Miku',
|
||||
name: 'Hatsune Miku',
|
||||
context: lastMsg!.content,
|
||||
content: "..."
|
||||
content: '...',
|
||||
});
|
||||
|
||||
const messageHistoryTxt = messageList.map(msg => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
|
||||
const messageHistoryTxt =
|
||||
messageList.map((msg) => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
|
||||
logInfo(`[openai] Requesting response for message history: ${messageHistoryTxt}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: "system", content: sysprompt },
|
||||
{ role: "user", content: USER_PROMPT + messageHistoryTxt }
|
||||
],
|
||||
temperature: params?.temperature || 0.5,
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: 'system', content: sysprompt },
|
||||
{ role: 'user', content: USER_PROMPT + messageHistoryTxt },
|
||||
],
|
||||
temperature: params?.temperature || 0.5,
|
||||
top_p: params?.top_p || 0.9,
|
||||
max_tokens: params?.max_new_tokens || 128,
|
||||
});
|
||||
|
||||
let content = response.choices[0].message.content;
|
||||
if (content.lastIndexOf('</think>') > -1) {
|
||||
content = content.slice(content.lastIndexOf('</think>') + 8);
|
||||
}
|
||||
if (content.lastIndexOf('</think>') > -1) {
|
||||
content = content.slice(content.lastIndexOf('</think>') + 8);
|
||||
}
|
||||
logInfo(`[openai] API response: ${content}`);
|
||||
|
||||
if (!content) {
|
||||
throw new TypeError("OpenAI API returned no message.");
|
||||
throw new TypeError('OpenAI API returned no message.');
|
||||
}
|
||||
|
||||
return content;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Message } from "discord.js";
|
||||
import { LLMConfig } from "../commands/types";
|
||||
import { Message } from 'discord.js';
|
||||
import { LLMConfig } from '../commands/types';
|
||||
|
||||
export interface LLMProvider
|
||||
{
|
||||
export interface LLMProvider {
|
||||
name(): string;
|
||||
requestLLMResponse(history: Message[], sysprompt: string, params: LLMConfig): Promise<string>;
|
||||
setModel(id: string);
|
||||
}
|
||||
|
||||
export interface LLMDiscordMessage
|
||||
{
|
||||
timestamp: string
|
||||
author: string
|
||||
name?: string
|
||||
context?: string
|
||||
content: string
|
||||
reactions?: string
|
||||
export interface LLMDiscordMessage {
|
||||
timestamp: string;
|
||||
author: string;
|
||||
name?: string;
|
||||
context?: string;
|
||||
content: string;
|
||||
reactions?: string;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import { logInfo } from '../logging';
|
||||
import { db, openDb, reactionEmojis, sync } from './util';
|
||||
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.GuildMessages],
|
||||
intents: [
|
||||
GatewayIntentBits.MessageContent,
|
||||
IntentsBitField.Flags.Guilds,
|
||||
IntentsBitField.Flags.GuildMessages,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||
});
|
||||
|
||||
@@ -20,12 +24,12 @@ client.once(Events.ClientReady, async () => {
|
||||
});
|
||||
|
||||
async function startup() {
|
||||
logInfo("[db] Opening...");
|
||||
logInfo('[db] Opening...');
|
||||
await openDb();
|
||||
logInfo("[db] Migrating...");
|
||||
logInfo('[db] Migrating...');
|
||||
await db.migrate();
|
||||
logInfo("[db] Ready.");
|
||||
logInfo("[bot] Logging in...");
|
||||
logInfo('[db] Ready.');
|
||||
logInfo('[bot] Logging in...');
|
||||
await client.login(process.env.TOKEN);
|
||||
await sync(client.guilds);
|
||||
process.exit(0);
|
||||
|
||||
102
discord/util.ts
102
discord/util.ts
@@ -3,7 +3,15 @@
|
||||
* Common helper functions
|
||||
*/
|
||||
|
||||
import { Collection, GuildManager, GuildTextBasedChannel, Message, MessageReaction, MessageType, User } from 'discord.js';
|
||||
import {
|
||||
Collection,
|
||||
GuildManager,
|
||||
GuildTextBasedChannel,
|
||||
Message,
|
||||
MessageReaction,
|
||||
MessageType,
|
||||
User,
|
||||
} from 'discord.js';
|
||||
import { get as getEmojiName } from 'emoji-unicode-map';
|
||||
import { createWriteStream, existsSync, unlinkSync } from 'fs';
|
||||
import { get as httpGet } from 'https';
|
||||
@@ -15,53 +23,49 @@ import { logError, logInfo, logWarn } from '../logging';
|
||||
import { ScoreboardMessageRow } from '../models';
|
||||
import { LLMDiscordMessage } from './provider/provider';
|
||||
|
||||
|
||||
const reactionEmojis: string[] = process.env.REACTIONS.split(',');
|
||||
let db: Database = null;
|
||||
|
||||
const REAL_NAMES = { // username to real name mapping
|
||||
'vinso1445': 'Vincent Iannelli',
|
||||
'scoliono': 'James Shiffer',
|
||||
'drugseller88': 'James Shiffer',
|
||||
'gnuwu': 'David Zheng',
|
||||
'f0oby': 'Myles Linden',
|
||||
'bapazheng': 'Myles Linden',
|
||||
'bapabakshi': 'Myles Linden',
|
||||
'keliande27': 'Myles Linden',
|
||||
const REAL_NAMES = {
|
||||
// username to real name mapping
|
||||
vinso1445: 'Vincent Iannelli',
|
||||
scoliono: 'James Shiffer',
|
||||
drugseller88: 'James Shiffer',
|
||||
gnuwu: 'David Zheng',
|
||||
f0oby: 'Myles Linden',
|
||||
bapazheng: 'Myles Linden',
|
||||
bapabakshi: 'Myles Linden',
|
||||
keliande27: 'Myles Linden',
|
||||
'1thinker': 'Samuel Habib',
|
||||
'adam28405': 'Adam Kazerounian',
|
||||
adam28405: 'Adam Kazerounian',
|
||||
'shibe.mp4': 'Jake Wong',
|
||||
'Hatsune Miku': 'Hatsune Miku'
|
||||
'Hatsune Miku': 'Hatsune Miku',
|
||||
};
|
||||
|
||||
|
||||
async function openDb() {
|
||||
db = await open({
|
||||
filename: 'db.sqlite',
|
||||
driver: Database3
|
||||
})
|
||||
driver: Database3,
|
||||
});
|
||||
}
|
||||
|
||||
function clearDb() {
|
||||
unlinkSync('db.sqlite');
|
||||
}
|
||||
|
||||
function messageLink(message: ScoreboardMessageRow)
|
||||
{
|
||||
function messageLink(message: ScoreboardMessageRow) {
|
||||
return `https://discord.com/channels/${message.guild}/${message.channel}/${message.id}`;
|
||||
}
|
||||
|
||||
function userAvatarPath(user: User)
|
||||
{
|
||||
function userAvatarPath(user: User) {
|
||||
return `../public/avatars/${user.id}.webp`;
|
||||
}
|
||||
|
||||
async function downloadUserAvatar(user: User)
|
||||
{
|
||||
async function downloadUserAvatar(user: User) {
|
||||
logInfo(`[bot] Downloading ${user.id}'s avatar...`);
|
||||
const file = createWriteStream(userAvatarPath(user));
|
||||
return new Promise<void>(resolve => {
|
||||
httpGet(user.displayAvatarURL(), res => {
|
||||
return new Promise<void>((resolve) => {
|
||||
httpGet(user.displayAvatarURL(), (res) => {
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
@@ -72,9 +76,8 @@ async function downloadUserAvatar(user: User)
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshUserReactionTotalCount(user: User, emoji_idx: number)
|
||||
{
|
||||
const result = await db.get<{sum: number}>(
|
||||
async function refreshUserReactionTotalCount(user: User, emoji_idx: number) {
|
||||
const result = await db.get<{ sum: number }>(
|
||||
`SELECT sum(reaction_${emoji_idx}_count) AS sum FROM messages WHERE author = ?`,
|
||||
user.id
|
||||
);
|
||||
@@ -98,8 +101,7 @@ async function refreshUserReactionTotalCount(user: User, emoji_idx: number)
|
||||
logInfo(`[bot] Refreshed ${user.id}'s ${emojiName} count.`);
|
||||
}
|
||||
|
||||
async function recordReaction(reaction: MessageReaction)
|
||||
{
|
||||
async function recordReaction(reaction: MessageReaction) {
|
||||
// Match emoji by name (unicode) or by ID (custom emoji)
|
||||
let emojiIdx = 0;
|
||||
const emojiName = reaction.emoji.name;
|
||||
@@ -107,7 +109,7 @@ async function recordReaction(reaction: MessageReaction)
|
||||
|
||||
if (emojiId) {
|
||||
// Custom emoji - match by ID
|
||||
emojiIdx = reactionEmojis.findIndex(e => e.includes(`:${emojiId}`)) + 1;
|
||||
emojiIdx = reactionEmojis.findIndex((e) => e.includes(`:${emojiId}`)) + 1;
|
||||
if (emojiIdx > 0) {
|
||||
logInfo(`[bot] Custom emoji detected: ${emojiName} (ID: ${emojiId}), idx: ${emojiIdx}`);
|
||||
}
|
||||
@@ -146,8 +148,7 @@ async function recordReaction(reaction: MessageReaction)
|
||||
}
|
||||
}
|
||||
|
||||
async function serializeMessageHistory(m: Message): Promise<LLMDiscordMessage | undefined>
|
||||
{
|
||||
async function serializeMessageHistory(m: Message): Promise<LLMDiscordMessage | undefined> {
|
||||
const stringifyReactions = (m: Message): string | undefined => {
|
||||
const reacts = m.reactions.cache;
|
||||
let serialized: string | undefined = undefined;
|
||||
@@ -176,7 +177,7 @@ async function serializeMessageHistory(m: Message): Promise<LLMDiscordMessage |
|
||||
name: REAL_NAMES[m.author.username] || null,
|
||||
context: undefined,
|
||||
content: m.cleanContent,
|
||||
reactions: stringifyReactions(m)
|
||||
reactions: stringifyReactions(m),
|
||||
};
|
||||
|
||||
// fetch replied-to message, if there is one
|
||||
@@ -192,7 +193,7 @@ async function serializeMessageHistory(m: Message): Promise<LLMDiscordMessage |
|
||||
}
|
||||
|
||||
return msgDict;
|
||||
};
|
||||
}
|
||||
|
||||
async function sync(guilds: GuildManager) {
|
||||
const guild = await guilds.fetch(process.env.GUILD);
|
||||
@@ -202,7 +203,9 @@ async function sync(guilds: GuildManager) {
|
||||
}
|
||||
logInfo(`[bot] Entered guild ${guild.id}`);
|
||||
const channels = await guild.channels.fetch();
|
||||
const textChannels = <Collection<string, GuildTextBasedChannel>> channels.filter(c => c && 'messages' in c && c.isTextBased);
|
||||
const textChannels = <Collection<string, GuildTextBasedChannel>>(
|
||||
channels.filter((c) => c && 'messages' in c && c.isTextBased)
|
||||
);
|
||||
for (const [id, textChannel] of textChannels) {
|
||||
logInfo(`[bot] Found text channel ${id}`);
|
||||
const oldestMsg = await db.get<ScoreboardMessageRow>(
|
||||
@@ -228,11 +231,13 @@ async function sync(guilds: GuildManager) {
|
||||
|
||||
newMessagesAfter = await textChannel.messages.fetch({ after, limit: 100 });
|
||||
messagesCount += newMessagesAfter.size;
|
||||
logInfo(`[bot] [${id}] Fetched ${messagesCount} messages (+${newMessagesBefore.size} older, ${newMessagesAfter.size} newer)`);
|
||||
logInfo(
|
||||
`[bot] [${id}] Fetched ${messagesCount} messages (+${newMessagesBefore.size} older, ${newMessagesAfter.size} newer)`
|
||||
);
|
||||
|
||||
const reactions = newMessagesBefore
|
||||
.flatMap<MessageReaction>(m => m.reactions.cache)
|
||||
.concat(newMessagesAfter.flatMap<MessageReaction>(m => m.reactions.cache));
|
||||
.flatMap<MessageReaction>((m) => m.reactions.cache)
|
||||
.concat(newMessagesAfter.flatMap<MessageReaction>((m) => m.reactions.cache));
|
||||
for (const [_, reaction] of reactions) {
|
||||
await recordReaction(reaction);
|
||||
}
|
||||
@@ -253,19 +258,28 @@ async function sync(guilds: GuildManager) {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestTTSResponse(txt: string): Promise<Blob>
|
||||
{
|
||||
async function requestTTSResponse(txt: string): Promise<Blob> {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("token", process.env.LLM_TOKEN);
|
||||
queryParams.append("text", txt);
|
||||
queryParams.append('token', process.env.LLM_TOKEN);
|
||||
queryParams.append('text', txt);
|
||||
|
||||
const ttsEndpoint = `${process.env.LLM_HOST}/tts?${queryParams.toString()}`;
|
||||
logInfo(`[bot] Requesting TTS response for "${txt}"`);
|
||||
const res = await fetch(ttsEndpoint, {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
});
|
||||
const resContents = await res.blob();
|
||||
return resContents;
|
||||
}
|
||||
|
||||
export { db, clearDb, openDb, reactionEmojis, recordReaction, requestTTSResponse, serializeMessageHistory, sync, REAL_NAMES };
|
||||
export {
|
||||
db,
|
||||
clearDb,
|
||||
openDb,
|
||||
reactionEmojis,
|
||||
recordReaction,
|
||||
requestTTSResponse,
|
||||
serializeMessageHistory,
|
||||
sync,
|
||||
REAL_NAMES,
|
||||
};
|
||||
|
||||
34
models.ts
34
models.ts
@@ -4,26 +4,26 @@
|
||||
*/
|
||||
|
||||
interface ScoreboardMessageRow {
|
||||
id: number,
|
||||
guild: number,
|
||||
channel: number,
|
||||
author: string,
|
||||
content: string,
|
||||
reaction_1_count: number,
|
||||
reaction_2_count: number,
|
||||
reaction_3_count: number,
|
||||
reaction_4_count: number,
|
||||
reaction_5_count: number
|
||||
id: number;
|
||||
guild: number;
|
||||
channel: number;
|
||||
author: string;
|
||||
content: string;
|
||||
reaction_1_count: number;
|
||||
reaction_2_count: number;
|
||||
reaction_3_count: number;
|
||||
reaction_4_count: number;
|
||||
reaction_5_count: number;
|
||||
}
|
||||
|
||||
interface ScoreboardUserRow {
|
||||
id: string,
|
||||
username: string,
|
||||
reaction_1_total: number,
|
||||
reaction_2_total: number,
|
||||
reaction_3_total: number,
|
||||
reaction_4_total: number,
|
||||
reaction_5_total: number
|
||||
id: string;
|
||||
username: string;
|
||||
reaction_1_total: number;
|
||||
reaction_2_total: number;
|
||||
reaction_3_total: number;
|
||||
reaction_4_total: number;
|
||||
reaction_5_total: number;
|
||||
}
|
||||
|
||||
export { ScoreboardMessageRow, ScoreboardUserRow };
|
||||
|
||||
5137
package-lock.json
generated
5137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -1,19 +1,24 @@
|
||||
{
|
||||
"name": "femscoreboard",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "npx tsc && node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"pug": "^3.0.2",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.18",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
"name": "femscoreboard",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "prettier --write .",
|
||||
"build": "tsc",
|
||||
"start": "npm run build && node server.js",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"pug": "^3.0.2",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.18",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
60
server.ts
60
server.ts
@@ -19,25 +19,59 @@ let db: Database = null;
|
||||
async function openDb() {
|
||||
return open({
|
||||
filename: 'discord/db.sqlite',
|
||||
driver: Database3
|
||||
driver: Database3,
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
const msg1 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_1_count DESC LIMIT 5');
|
||||
const msg2 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_2_count DESC LIMIT 5');
|
||||
const msg3 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_3_count DESC LIMIT 5');
|
||||
const msg4 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_4_count DESC LIMIT 5');
|
||||
const msg5 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_5_count DESC LIMIT 5');
|
||||
const bestMsg = await db.all<[ScoreboardMessageRow]>('SELECT *, SUM(reaction_1_count)+SUM(reaction_2_count)+SUM(reaction_3_count)+SUM(reaction_4_count)+SUM(reaction_5_count) AS all_reacts FROM messages GROUP BY id ORDER BY all_reacts DESC LIMIT 5');
|
||||
const msg1 = await db.all<[ScoreboardMessageRow]>(
|
||||
'SELECT * FROM messages ORDER BY reaction_1_count DESC LIMIT 5'
|
||||
);
|
||||
const msg2 = await db.all<[ScoreboardMessageRow]>(
|
||||
'SELECT * FROM messages ORDER BY reaction_2_count DESC LIMIT 5'
|
||||
);
|
||||
const msg3 = await db.all<[ScoreboardMessageRow]>(
|
||||
'SELECT * FROM messages ORDER BY reaction_3_count DESC LIMIT 5'
|
||||
);
|
||||
const msg4 = await db.all<[ScoreboardMessageRow]>(
|
||||
'SELECT * FROM messages ORDER BY reaction_4_count DESC LIMIT 5'
|
||||
);
|
||||
const msg5 = await db.all<[ScoreboardMessageRow]>(
|
||||
'SELECT * FROM messages ORDER BY reaction_5_count DESC LIMIT 5'
|
||||
);
|
||||
const bestMsg = await db.all<[ScoreboardMessageRow]>(
|
||||
'SELECT *, SUM(reaction_1_count)+SUM(reaction_2_count)+SUM(reaction_3_count)+SUM(reaction_4_count)+SUM(reaction_5_count) AS all_reacts FROM messages GROUP BY id ORDER BY all_reacts DESC LIMIT 5'
|
||||
);
|
||||
|
||||
const funniest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_1_total DESC LIMIT 15');
|
||||
const realest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_2_total DESC LIMIT 15');
|
||||
const cunniest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_3_total DESC LIMIT 15');
|
||||
const based = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_4_total DESC LIMIT 15');
|
||||
const agreeable = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_5_total DESC LIMIT 15');
|
||||
const funniest = await db.all<[ScoreboardUserRow]>(
|
||||
'SELECT * FROM users ORDER BY reaction_1_total DESC LIMIT 15'
|
||||
);
|
||||
const realest = await db.all<[ScoreboardUserRow]>(
|
||||
'SELECT * FROM users ORDER BY reaction_2_total DESC LIMIT 15'
|
||||
);
|
||||
const cunniest = await db.all<[ScoreboardUserRow]>(
|
||||
'SELECT * FROM users ORDER BY reaction_3_total DESC LIMIT 15'
|
||||
);
|
||||
const based = await db.all<[ScoreboardUserRow]>(
|
||||
'SELECT * FROM users ORDER BY reaction_4_total DESC LIMIT 15'
|
||||
);
|
||||
const agreeable = await db.all<[ScoreboardUserRow]>(
|
||||
'SELECT * FROM users ORDER BY reaction_5_total DESC LIMIT 15'
|
||||
);
|
||||
|
||||
res.render('index', { funniest, realest, cunniest, based, agreeable, msg1, msg2, msg3, msg4, msg5, bestMsg });
|
||||
res.render('index', {
|
||||
funniest,
|
||||
realest,
|
||||
cunniest,
|
||||
based,
|
||||
agreeable,
|
||||
msg1,
|
||||
msg2,
|
||||
msg3,
|
||||
msg4,
|
||||
msg5,
|
||||
bestMsg,
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(port, async () => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2020",
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"discord/node_modules"
|
||||
]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2020",
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": ["discord/node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user