183 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			183 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * util.ts
 | 
						|
 * Common helper functions
 | 
						|
 */
 | 
						|
 | 
						|
import { Collection, GuildManager, GuildTextBasedChannel, Message, MessageReaction, User } from 'discord.js';
 | 
						|
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';
 | 
						|
import fetch from 'node-fetch';
 | 
						|
import { logError, logInfo, logWarn } from '../logging';
 | 
						|
import { ScoreboardMessageRow } from '../models';
 | 
						|
 | 
						|
 | 
						|
const reactionEmojis: string[] = process.env.REACTIONS.split(',');
 | 
						|
let db: Database = null;
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
{
 | 
						|
    logInfo(`[bot] Downloading ${user.id}'s avatar...`);
 | 
						|
    const file = createWriteStream(userAvatarPath(user));
 | 
						|
    return new Promise<void>(resolve => {
 | 
						|
        httpGet(user.displayAvatarURL(), res => {
 | 
						|
            res.pipe(file);
 | 
						|
            file.on('finish', () => {
 | 
						|
                file.close();
 | 
						|
                logInfo(`[bot] Finished downloading ${user.id}'s avatar.`);
 | 
						|
                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);
 | 
						|
    }
 | 
						|
    logInfo(`[bot] Refreshed ${user.id}'s ${reactionEmojis[emoji_idx - 1]} count.`);
 | 
						|
}
 | 
						|
 | 
						|
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(?, ?, ?, ?, ?, ?)
 | 
						|
            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,
 | 
						|
            reaction.message.id
 | 
						|
        );
 | 
						|
        await refreshUserReactionTotalCount(reaction.message.author, emojiIdx);
 | 
						|
        logInfo(`[bot] Recorded ${reaction.emoji.name}x${reaction.count} in database.`);
 | 
						|
    } catch (error) {
 | 
						|
        logError('[bot] Something went wrong when updating the database:', error);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
async function sync(guilds: GuildManager) {
 | 
						|
    const guild = await guilds.fetch(process.env.GUILD);
 | 
						|
    if (!guild) {
 | 
						|
        logError(`[bot] FATAL: guild ${guild.id} not found!`);
 | 
						|
        return 1;
 | 
						|
    }
 | 
						|
    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) {
 | 
						|
        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;
 | 
						|
                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));
 | 
						|
                for (const [_, reaction] of reactions) {
 | 
						|
                    await recordReaction(reaction);
 | 
						|
                }
 | 
						|
                reactionsCount += reactions.size;
 | 
						|
                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);
 | 
						|
            logInfo(`[bot] [${id}] Done.`);
 | 
						|
        } catch (err) {
 | 
						|
            logWarn(`[bot] [${id}] Failed to fetch messages and reactions: ${err}`);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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 };
 |