FemScoreboard/discord/util.ts

183 lines
6.5 KiB
TypeScript
Raw Permalink Normal View History

2023-10-07 22:46:02 -07:00
/**
* util.ts
* Common helper functions
*/
import { Collection, GuildManager, GuildTextBasedChannel, Message, MessageReaction, User } from 'discord.js';
2023-10-07 22:46:02 -07:00
import { createWriteStream, existsSync, unlinkSync } from 'fs';
import { get as httpGet } from 'https';
import { Database, open } from 'sqlite';
import { Database as Database3 } from 'sqlite3';
import 'dotenv/config';
2024-05-09 16:20:14 -07:00
import fetch from 'node-fetch';
2023-10-08 19:20:00 -07:00
import { logError, logInfo, logWarn } from '../logging';
2023-10-07 22:46:02 -07:00
import { ScoreboardMessageRow } from '../models';
const reactionEmojis: string[] = process.env.REACTIONS.split(',');
let db: Database = null;
2023-10-08 19:10:47 -07:00
2023-10-07 22:46:02 -07:00
async function openDb() {
db = await open({
filename: 'db.sqlite',
driver: Database3
})
}
function clearDb() {
unlinkSync('db.sqlite');
}
function messageLink(message: ScoreboardMessageRow)
{
return `https://discord.com/channels/${message.guild}/${message.channel}/${message.id}`;
}
function userAvatarPath(user: User)
{
return `../public/avatars/${user.id}.webp`;
}
async function downloadUserAvatar(user: User)
{
2023-10-08 19:10:47 -07:00
logInfo(`[bot] Downloading ${user.id}'s avatar...`);
2023-10-07 22:46:02 -07:00
const file = createWriteStream(userAvatarPath(user));
return new Promise<void>(resolve => {
httpGet(user.displayAvatarURL(), res => {
2023-10-07 22:46:02 -07:00
res.pipe(file);
file.on('finish', () => {
file.close();
2023-10-08 19:10:47 -07:00
logInfo(`[bot] Finished downloading ${user.id}'s avatar.`);
2023-10-07 22:46:02 -07:00
resolve();
});
});
});
}
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
);
const emojiTotal = result.sum;
await db.run(
`INSERT INTO users(id, username, reaction_${emoji_idx}_total) VALUES(?, ?, ?) ON CONFLICT(id) DO
UPDATE SET reaction_${emoji_idx}_total = ?, username = ? WHERE id = ?`,
user.id,
user.displayName,
emojiTotal,
emojiTotal,
user.displayName,
user.id
);
if (!existsSync(userAvatarPath(user))) {
await downloadUserAvatar(user);
}
2023-10-08 19:10:47 -07:00
logInfo(`[bot] Refreshed ${user.id}'s ${reactionEmojis[emoji_idx - 1]} count.`);
2023-10-07 22:46:02 -07:00
}
async function recordReaction(reaction: MessageReaction)
{
const emojiIdx = reactionEmojis.indexOf(reaction.emoji.name) + 1;
if (emojiIdx === 0) {
return;
}
try {
await db.run(
`INSERT INTO messages(id, guild, channel, author, content, reaction_${emojiIdx}_count) VALUES(?, ?, ?, ?, ?, ?)
2023-10-07 22:46:02 -07:00
ON CONFLICT(id) DO UPDATE SET reaction_${emojiIdx}_count = ? WHERE id = ?`,
reaction.message.id,
reaction.message.guildId,
reaction.message.channelId,
reaction.message.author.id,
reaction.message.content,
reaction.count,
reaction.count,
2023-10-07 22:46:02 -07:00
reaction.message.id
);
await refreshUserReactionTotalCount(reaction.message.author, emojiIdx);
2023-10-08 19:10:47 -07:00
logInfo(`[bot] Recorded ${reaction.emoji.name}x${reaction.count} in database.`);
2023-10-07 22:46:02 -07:00
} catch (error) {
2023-10-08 19:10:47 -07:00
logError('[bot] Something went wrong when updating the database:', error);
2023-10-07 22:46:02 -07:00
return;
}
}
async function sync(guilds: GuildManager) {
const guild = await guilds.fetch(process.env.GUILD);
if (!guild) {
2023-10-08 19:10:47 -07:00
logError(`[bot] FATAL: guild ${guild.id} not found!`);
return 1;
}
2023-10-08 19:10:47 -07:00
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);
for (const [id, textChannel] of textChannels) {
2023-10-08 19:10:47 -07:00
logInfo(`[bot] Found text channel ${id}`);
const oldestMsg = await db.get<ScoreboardMessageRow>(
'SELECT * FROM messages WHERE guild = ? AND channel = ? ORDER BY id ASC LIMIT 1',
guild.id,
id
);
const newestMsg = await db.get<ScoreboardMessageRow>(
'SELECT * FROM messages WHERE guild = ? AND channel = ? ORDER BY id DESC LIMIT 1',
guild.id,
id
);
let before: string = oldestMsg && String(oldestMsg.id);
let after: string = newestMsg && String(newestMsg.id);
let messagesCount = 0;
let reactionsCount = 0;
let newMessagesBefore: Collection<string, Message<true>>;
let newMessagesAfter: Collection<string, Message<true>>;
try {
do {
newMessagesBefore = await textChannel.messages.fetch({ before, limit: 100 });
messagesCount += newMessagesBefore.size;
newMessagesAfter = await textChannel.messages.fetch({ after, limit: 100 });
messagesCount += newMessagesAfter.size;
2023-10-08 19:10:47 -07:00
logInfo(`[bot] [${id}] Fetched ${messagesCount} messages (+${newMessagesBefore.size} older, ${newMessagesAfter.size} newer)`);
2023-10-08 19:10:47 -07:00
const reactions = newMessagesBefore
.flatMap<MessageReaction>(m => m.reactions.cache)
.concat(newMessagesAfter.flatMap<MessageReaction>(m => m.reactions.cache));
for (const [_, reaction] of reactions) {
await recordReaction(reaction);
}
reactionsCount += reactions.size;
2023-10-08 19:10:47 -07:00
logInfo(`[bot] [${id}] Recorded ${reactionsCount} reactions (+${reactions.size}).`);
if (newMessagesBefore.size > 0) {
before = newMessagesBefore.last().id;
}
if (newMessagesAfter.size > 0) {
after = newMessagesAfter.first().id;
}
} while (newMessagesBefore.size === 100 || newMessagesAfter.size === 100);
2023-10-08 19:10:47 -07:00
logInfo(`[bot] [${id}] Done.`);
} catch (err) {
2023-10-08 19:10:47 -07:00
logWarn(`[bot] [${id}] Failed to fetch messages and reactions: ${err}`);
}
}
}
2024-05-09 16:20:14 -07:00
async function requestTTSResponse(txt: string): Promise<Blob>
{
const queryParams = new URLSearchParams();
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'
});
const resContents = await res.blob();
return resContents;
}
export { db, clearDb, openDb, reactionEmojis, recordReaction, requestTTSResponse, sync };