mirror of
https://github.com/pacnpal/markov-discord.git
synced 2025-12-20 11:01:04 -05:00
14
.github/workflows/build-and-push-image.yml
vendored
14
.github/workflows/build-and-push-image.yml
vendored
@@ -12,25 +12,25 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: charlocharlie
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Build and push master
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
target: deploy
|
||||
push: true
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Build and push dev
|
||||
if: ${{ github.ref == 'refs/heads/develop' }}
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
target: deploy
|
||||
push: true
|
||||
|
||||
4
.github/workflows/dockerhub-description.yml
vendored
4
.github/workflows/dockerhub-description.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v2
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: charlocharlie
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
8
.github/workflows/typedoc.yml
vendored
8
.github/workflows/typedoc.yml
vendored
@@ -12,11 +12,11 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js for use with actions
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: NPM install
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: npm run docs
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -4,7 +4,7 @@
|
||||
"typescript"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
|
||||
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## Versions
|
||||
|
||||
### 2.3.0
|
||||
|
||||
* Update to Node 20 and Discord.js 14. Update a million dependencies
|
||||
* Fix empty attachment bug (#61)
|
||||
|
||||
### 2.2.0
|
||||
|
||||
* Add a `clean` option flag to the `/train` command to allow retraining without overwriting
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
########
|
||||
# BASE
|
||||
########
|
||||
FROM node:16-alpine3.15 as base
|
||||
FROM node:20-alpine3.20 as base
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
@@ -16,7 +16,7 @@ FROM base as prodDeps
|
||||
COPY package*.json ./
|
||||
# Install build tools for erlpack, then install prod deps only
|
||||
RUN apk add --no-cache make gcc g++ python3 \
|
||||
&& npm ci --only=production
|
||||
&& npm ci --omit=dev
|
||||
|
||||
########
|
||||
# BUILD
|
||||
@@ -26,7 +26,7 @@ FROM base as build
|
||||
COPY package*.json ./
|
||||
# Install build tools for erlpack, then install prod deps only
|
||||
RUN apk add --no-cache make gcc g++ python3 \
|
||||
&& npm ci --only=production
|
||||
&& npm ci --omit=dev
|
||||
|
||||
# Copy all jsons
|
||||
COPY package*.json tsconfig.json ./
|
||||
|
||||
5992
package-lock.json
generated
5992
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markov-discord",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"description": "A conversational Markov chain bot for Discord",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
@@ -31,50 +31,46 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^0.13.0",
|
||||
"@discordjs/rest": "^0.4.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"better-sqlite3": "^7.5.3",
|
||||
"bufferutil": "^4.0.6",
|
||||
"better-sqlite3": "^8.7.0",
|
||||
"bufferutil": "^4.0.8",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"discord-api-types": "^0.33.0",
|
||||
"discord.js": "^13.7.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"erlpack": "github:discord/erlpack",
|
||||
"fs-extra": "^10.1.0",
|
||||
"json5": "^2.2.2",
|
||||
"discord.js": "^14.15.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"fs-extra": "^11.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"markov-strings-db": "^4.2.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"pino": "^7.11.0",
|
||||
"pino-pretty": "^7.6.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"simple-eta": "^3.0.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typeorm": "^0.3.14",
|
||||
"utf-8-validate": "^5.0.9",
|
||||
"zlib-sync": "^0.1.7"
|
||||
"typeorm": "^0.3.20",
|
||||
"utf-8-validate": "^6.0.4",
|
||||
"zlib-sync": "^0.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.36",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
||||
"@typescript-eslint/parser": "^5.25.0",
|
||||
"eslint": "^8.16.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/validator": "^13.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"pm2": "^5.3.1",
|
||||
"prettier": "^2.6.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"typedoc": "^0.22.15",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"pm2": "^5.4.2",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.4",
|
||||
"types-package-json": "^2.0.39",
|
||||
"typescript": "^4.6.4"
|
||||
"typescript": "5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { SlashCommandBuilder, SlashCommandChannelOption } from '@discordjs/builders';
|
||||
import { REST } from '@discordjs/rest';
|
||||
import { ChannelType, Routes } from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandChannelOption,
|
||||
SlashCommandBuilder,
|
||||
ChannelType,
|
||||
Routes,
|
||||
REST,
|
||||
} from 'discord.js';
|
||||
import { config } from './config';
|
||||
import { packageJson } from './util';
|
||||
|
||||
@@ -18,21 +22,21 @@ export const messageCommand = new SlashCommandBuilder()
|
||||
.setName(config.slashCommandName)
|
||||
.setDescription('Generate a message from learned past messages')
|
||||
.addBooleanOption((tts) =>
|
||||
tts.setName('tts').setDescription('Read the message via text-to-speech.').setRequired(false)
|
||||
tts.setName('tts').setDescription('Read the message via text-to-speech.').setRequired(false),
|
||||
)
|
||||
.addBooleanOption((debug) =>
|
||||
debug
|
||||
.setName('debug')
|
||||
.setDescription('Follow up the generated message with the detailed sources that inspired it.')
|
||||
.setRequired(false)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addStringOption((seed) =>
|
||||
seed
|
||||
.setName('seed')
|
||||
.setDescription(
|
||||
`A ${config.stateSize}-word phrase to attempt to start a generated sentence with.`
|
||||
`A ${config.stateSize}-word phrase to attempt to start a generated sentence with.`,
|
||||
)
|
||||
.setRequired(false)
|
||||
.setRequired(false),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -52,10 +56,10 @@ export const listenChannelCommand = new SlashCommandBuilder()
|
||||
sub
|
||||
.setName('add')
|
||||
.setDescription(
|
||||
`Add channels to learn from. Doesn't add the channel's past messages; re-train to do that.`
|
||||
`Add channels to learn from. Doesn't add the channel's past messages; re-train to do that.`,
|
||||
);
|
||||
Array.from(Array(CHANNEL_OPTIONS_MAX).keys()).forEach((index) =>
|
||||
sub.addChannelOption((opt) => channelOptionsGenerator(opt, index))
|
||||
sub.addChannelOption((opt) => channelOptionsGenerator(opt, index)),
|
||||
);
|
||||
return sub;
|
||||
})
|
||||
@@ -63,42 +67,42 @@ export const listenChannelCommand = new SlashCommandBuilder()
|
||||
sub
|
||||
.setName('remove')
|
||||
.setDescription(
|
||||
`Remove channels from being learned from. Doesn't remove the channel's data; re-train to do that.`
|
||||
`Remove channels from being learned from. Doesn't remove the channel's data; re-train to do that.`,
|
||||
);
|
||||
Array.from(Array(CHANNEL_OPTIONS_MAX).keys()).forEach((index) =>
|
||||
sub.addChannelOption((opt) => channelOptionsGenerator(opt, index))
|
||||
sub.addChannelOption((opt) => channelOptionsGenerator(opt, index)),
|
||||
);
|
||||
return sub;
|
||||
})
|
||||
.addSubcommand((sub) =>
|
||||
sub
|
||||
.setName('list')
|
||||
.setDescription(`List the channels the bot is currently actively listening to.`)
|
||||
.setDescription(`List the channels the bot is currently actively listening to.`),
|
||||
)
|
||||
.addSubcommand((sub) =>
|
||||
sub
|
||||
.setName('modify')
|
||||
.setDescription(`Add or remove channels via select menu UI (first 25 text channels only)`)
|
||||
.setDescription(`Add or remove channels via select menu UI (first 25 text channels only)`),
|
||||
);
|
||||
|
||||
export const trainCommand = new SlashCommandBuilder()
|
||||
.setName('train')
|
||||
.setDescription(
|
||||
'Train from past messages from the configured listened channels. This takes a while.'
|
||||
'Train from past messages from the configured listened channels. This takes a while.',
|
||||
)
|
||||
.addBooleanOption((clean) =>
|
||||
clean
|
||||
.setName('clean')
|
||||
.setDescription(
|
||||
'Whether the database should be emptied before training. Default is true (recommended).'
|
||||
'Whether the database should be emptied before training. Default is true (recommended).',
|
||||
)
|
||||
.setRequired(false)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addAttachmentOption((json) =>
|
||||
json
|
||||
.setName('json')
|
||||
.setDescription('Train from a provided JSON file rather than channel history.')
|
||||
.setRequired(false)
|
||||
.setRequired(false),
|
||||
);
|
||||
|
||||
const commands = [
|
||||
|
||||
203
src/index.ts
203
src/index.ts
@@ -12,8 +12,6 @@ import type { PackageJsonPerson } from 'types-package-json';
|
||||
import makeEta from 'simple-eta';
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import addSeconds from 'date-fns/addSeconds';
|
||||
import type { APIInteractionGuildMember, APISelectMenuComponent } from 'discord-api-types/v9';
|
||||
import fetch from 'node-fetch';
|
||||
import L from './logger';
|
||||
import { Channel } from './entity/Channel';
|
||||
import { Guild } from './entity/Guild';
|
||||
@@ -40,21 +38,30 @@ interface SelectMenuChannel {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface IRefreshUrlsRes {
|
||||
refreshed_urls: Array<{
|
||||
original: string;
|
||||
refreshed: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply options that can be used in both MessageOptions and InteractionReplyOptions
|
||||
*/
|
||||
type AgnosticReplyOptions = Omit<Discord.MessageOptions, 'reply' | 'stickers' | 'flags'>;
|
||||
type AgnosticReplyOptions = Omit<Discord.MessageCreateOptions, 'reply' | 'stickers' | 'flags'>;
|
||||
|
||||
const INVALID_PERMISSIONS_MESSAGE = 'You do not have the permissions for this action.';
|
||||
const INVALID_GUILD_MESSAGE = 'This action must be performed within a server.';
|
||||
|
||||
const rest = new Discord.REST({ version: '10' }).setToken(config.token);
|
||||
|
||||
const client = new Discord.Client<true>({
|
||||
failIfNotExists: false,
|
||||
intents: [Discord.Intents.FLAGS.GUILD_MESSAGES, Discord.Intents.FLAGS.GUILDS],
|
||||
intents: [Discord.GatewayIntentBits.GuildMessages, Discord.GatewayIntentBits.Guilds],
|
||||
presence: {
|
||||
activities: [
|
||||
{
|
||||
type: 'PLAYING',
|
||||
type: Discord.ActivityType.Playing,
|
||||
name: config.activity,
|
||||
url: packageJson().homepage,
|
||||
},
|
||||
@@ -75,6 +82,14 @@ const markovGenerateOptions: MarkovGenerateOptions<MarkovDataCustom> = {
|
||||
maxTries: config.maxTries,
|
||||
};
|
||||
|
||||
async function refreshCdnUrl(url: string): Promise<string> {
|
||||
// Thank you https://github.com/ShufflePerson/Discord_CDN
|
||||
const resp = (await rest.post(`/attachments/refresh-urls`, {
|
||||
body: { attachment_urls: [url] },
|
||||
})) as IRefreshUrlsRes;
|
||||
return resp.refreshed_urls[0].refreshed;
|
||||
}
|
||||
|
||||
async function getMarkovByGuildId(guildId: string): Promise<Markov> {
|
||||
const markov = new Markov({ id: guildId, options: { ...markovOpts, id: guildId } });
|
||||
L.trace({ guildId }, 'Setting up markov instance');
|
||||
@@ -117,7 +132,7 @@ async function getValidChannels(guild: Discord.Guild): Promise<Discord.TextChann
|
||||
L.error({ erroredChannel: dbc, channelId }, 'Error fetching channel');
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
).filter((c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel);
|
||||
return channels;
|
||||
@@ -127,7 +142,7 @@ async function getTextChannels(guild: Discord.Guild): Promise<SelectMenuChannel[
|
||||
L.trace('Getting text channels for select menu');
|
||||
const MAX_SELECT_OPTIONS = 25;
|
||||
const textChannels = guild.channels.cache.filter(
|
||||
(c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel
|
||||
(c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel,
|
||||
);
|
||||
const foundDbChannels = await Channel.findByIds(Array.from(textChannels.keys()));
|
||||
const foundDbChannelsWithName: SelectMenuChannel[] = foundDbChannels.map((c) => ({
|
||||
@@ -153,7 +168,7 @@ async function addValidChannels(channels: Discord.TextChannel[], guildId: string
|
||||
|
||||
async function removeValidChannels(
|
||||
channels: Discord.TextChannel[],
|
||||
guildId: string
|
||||
guildId: string,
|
||||
): Promise<void> {
|
||||
L.trace(`Removing ${channels.length} channels from valid list`);
|
||||
const dbChannels = channels.map((c) => {
|
||||
@@ -168,12 +183,14 @@ async function removeValidChannels(
|
||||
* @return {Boolean} True if the sender is a moderator.
|
||||
*
|
||||
*/
|
||||
function isModerator(member: Discord.GuildMember | APIInteractionGuildMember | null): boolean {
|
||||
function isModerator(
|
||||
member: Discord.GuildMember | Discord.APIInteractionGuildMember | null,
|
||||
): boolean {
|
||||
const MODERATOR_PERMISSIONS: Discord.PermissionResolvable[] = [
|
||||
'ADMINISTRATOR',
|
||||
'MANAGE_CHANNELS',
|
||||
'KICK_MEMBERS',
|
||||
'MOVE_MEMBERS',
|
||||
'Administrator',
|
||||
'ManageChannels',
|
||||
'KickMembers',
|
||||
'MoveMembers',
|
||||
];
|
||||
if (!member) return false;
|
||||
if (member instanceof Discord.GuildMember) {
|
||||
@@ -193,7 +210,9 @@ function isModerator(member: Discord.GuildMember | APIInteractionGuildMember | n
|
||||
* @return {Boolean} True if the sender is a moderator.
|
||||
*
|
||||
*/
|
||||
function isAllowedUser(member: Discord.GuildMember | APIInteractionGuildMember | null): boolean {
|
||||
function isAllowedUser(
|
||||
member: Discord.GuildMember | Discord.APIInteractionGuildMember | null,
|
||||
): boolean {
|
||||
if (!config.userRoleIds.length) return true;
|
||||
if (!member) return false;
|
||||
if (member instanceof Discord.GuildMember) {
|
||||
@@ -255,7 +274,7 @@ function messageToData(message: Discord.Message): AddDataProps {
|
||||
*/
|
||||
async function saveGuildMessageHistory(
|
||||
interaction: Discord.Message | Discord.CommandInteraction,
|
||||
clean = true
|
||||
clean = true,
|
||||
): Promise<string> {
|
||||
if (!isModerator(interaction.member)) return INVALID_PERMISSIONS_MESSAGE;
|
||||
if (!interaction.guildId || !interaction.guild) return INVALID_GUILD_MESSAGE;
|
||||
@@ -280,31 +299,31 @@ async function saveGuildMessageHistory(
|
||||
const messageContent = `Parsing past messages from ${channels.length} channel(s).`;
|
||||
|
||||
const NO_COMPLETED_CHANNELS_TEXT = 'None';
|
||||
const completedChannelsField: Discord.EmbedFieldData = {
|
||||
const completedChannelsField: Discord.APIEmbedField = {
|
||||
name: 'Completed Channels',
|
||||
value: NO_COMPLETED_CHANNELS_TEXT,
|
||||
inline: true,
|
||||
};
|
||||
const currentChannelField: Discord.EmbedFieldData = {
|
||||
const currentChannelField: Discord.APIEmbedField = {
|
||||
name: 'Current Channel',
|
||||
value: `<#${channels[0].id}>`,
|
||||
inline: true,
|
||||
};
|
||||
const currentChannelPercent: Discord.EmbedFieldData = {
|
||||
const currentChannelPercent: Discord.APIEmbedField = {
|
||||
name: 'Channel Progress',
|
||||
value: '0%',
|
||||
inline: true,
|
||||
};
|
||||
const currentChannelEta: Discord.EmbedFieldData = {
|
||||
const currentChannelEta: Discord.APIEmbedField = {
|
||||
name: 'Channel Time Remaining',
|
||||
value: 'Pending...',
|
||||
inline: true,
|
||||
};
|
||||
const embedOptions: Discord.MessageEmbedOptions = {
|
||||
const embedOptions: Discord.EmbedData = {
|
||||
title: 'Training Progress',
|
||||
fields: [completedChannelsField, currentChannelField, currentChannelPercent, currentChannelEta],
|
||||
};
|
||||
const embed = new Discord.MessageEmbed(embedOptions);
|
||||
const embed = new Discord.EmbedBuilder(embedOptions);
|
||||
let progressMessage: Discord.Message;
|
||||
const updateMessageData = { content: messageContent, embeds: [embed] };
|
||||
if (interaction instanceof Discord.Message) {
|
||||
@@ -338,7 +357,7 @@ async function saveGuildMessageHistory(
|
||||
} catch (err) {
|
||||
L.error(err);
|
||||
L.error(
|
||||
`Error retreiving messages before ${oldestMessageID} in channel ${channel.name}. This is probably a permissions issue.`
|
||||
`Error retreiving messages before ${oldestMessageID} in channel ${channel.name}. This is probably a permissions issue.`,
|
||||
);
|
||||
break; // Give up on this channel
|
||||
}
|
||||
@@ -347,7 +366,7 @@ async function saveGuildMessageHistory(
|
||||
const threadChannels = channelBatchMessages
|
||||
.filter((m) => m.hasThread)
|
||||
.map((m) => m.thread)
|
||||
.filter((c): c is Discord.ThreadChannel => c !== null);
|
||||
.filter((c): c is Discord.AnyThreadChannel => c !== null);
|
||||
if (threadChannels.length > 0) {
|
||||
L.debug(`Found ${threadChannels.length} threads. Reading into them.`);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@@ -367,13 +386,13 @@ async function saveGuildMessageHistory(
|
||||
} catch (err) {
|
||||
L.error(err);
|
||||
L.error(
|
||||
`Error retreiving thread messages before ${oldestThreadMessageID} in thread ${threadChannel.name}. This is probably a permissions issue.`
|
||||
`Error retreiving thread messages before ${oldestThreadMessageID} in thread ${threadChannel.name}. This is probably a permissions issue.`,
|
||||
);
|
||||
break; // Give up on this thread
|
||||
}
|
||||
L.trace(
|
||||
{ threadMessagesCount: threadBatchMessages.size },
|
||||
`Found some thread messages`
|
||||
`Found some thread messages`,
|
||||
);
|
||||
const lastThreadMessage = threadBatchMessages.last();
|
||||
allBatchMessages = allBatchMessages.concat(threadBatchMessages); // Add the thread messages to this message batch to be included in later processing
|
||||
@@ -431,12 +450,12 @@ async function saveGuildMessageHistory(
|
||||
lastUpdate = messagesCount;
|
||||
L.debug(
|
||||
{ messagesCount, pctComplete: currentChannelPercent.value },
|
||||
'Sending metrics update'
|
||||
'Sending metrics update',
|
||||
);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await progressMessage.edit({
|
||||
...updateMessageData,
|
||||
embeds: [new Discord.MessageEmbed(embedOptions)],
|
||||
embeds: [new Discord.EmbedBuilder(embedOptions)],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -455,9 +474,9 @@ interface JSONImport {
|
||||
* Train from an attached JSON file
|
||||
*/
|
||||
async function trainFromAttachmentJson(
|
||||
attachment: Discord.MessageAttachment,
|
||||
attachmentUrl: string,
|
||||
interaction: Discord.CommandInteraction,
|
||||
clean = true
|
||||
clean = true,
|
||||
): Promise<string> {
|
||||
if (!isModerator(interaction.member)) return INVALID_PERMISSIONS_MESSAGE;
|
||||
if (!interaction.guildId || !interaction.guild) return INVALID_GUILD_MESSAGE;
|
||||
@@ -466,8 +485,7 @@ async function trainFromAttachmentJson(
|
||||
|
||||
let trainingData: AddDataProps[];
|
||||
try {
|
||||
const importAttachmentUrl = attachment.attachment.toString();
|
||||
const getResp = await fetch(importAttachmentUrl);
|
||||
const getResp = await fetch(attachmentUrl);
|
||||
if (!getResp.ok) throw new Error(getResp.statusText);
|
||||
const importData = (await getResp.json()) as JSONImport[];
|
||||
|
||||
@@ -480,7 +498,7 @@ async function trainFromAttachmentJson(
|
||||
}
|
||||
if (datum.attachments?.every((a) => typeof a !== 'string')) {
|
||||
throw new Error(
|
||||
`Entry at index ${index} must have all "attachments" each with a type of string`
|
||||
`Entry at index ${index} must have all "attachments" each with a type of string`,
|
||||
);
|
||||
}
|
||||
let custom: MarkovDataCustom | undefined;
|
||||
@@ -530,7 +548,7 @@ interface GenerateOptions {
|
||||
*/
|
||||
async function generateResponse(
|
||||
interaction: Discord.Message | Discord.CommandInteraction,
|
||||
options?: GenerateOptions
|
||||
options?: GenerateOptions,
|
||||
): Promise<GenerateResponse> {
|
||||
L.debug({ options }, 'Responding...');
|
||||
const { tts = false, debug = false, startSeed } = options || {};
|
||||
@@ -558,7 +576,8 @@ async function generateResponse(
|
||||
.flatMap((ref) => (ref.custom as MarkovDataCustom).attachments);
|
||||
if (attachmentUrls.length > 0) {
|
||||
const randomRefAttachment = getRandomElement(attachmentUrls);
|
||||
messageOpts.files = [randomRefAttachment];
|
||||
const refreshedUrl = await refreshCdnUrl(randomRefAttachment);
|
||||
messageOpts.files = [refreshedUrl];
|
||||
} else {
|
||||
const randomMessage = await MarkovInputData.createQueryBuilder<
|
||||
MarkovInputData<MarkovDataCustom>
|
||||
@@ -570,7 +589,9 @@ async function generateResponse(
|
||||
.getOne();
|
||||
const randomMessageAttachmentUrls = randomMessage?.custom?.attachments;
|
||||
if (randomMessageAttachmentUrls?.length) {
|
||||
messageOpts.files = [{ attachment: getRandomElement(randomMessageAttachmentUrls) }];
|
||||
const attachmentUrl = getRandomElement(randomMessageAttachmentUrls);
|
||||
const refreshedUrl = await refreshCdnUrl(attachmentUrl);
|
||||
messageOpts.files = [{ attachment: refreshedUrl }];
|
||||
}
|
||||
}
|
||||
messageOpts.content = response.string;
|
||||
@@ -606,52 +627,59 @@ async function listValidChannels(interaction: Discord.CommandInteraction): Promi
|
||||
}
|
||||
|
||||
function getChannelsFromInteraction(
|
||||
interaction: Discord.CommandInteraction
|
||||
interaction: Discord.ChatInputCommandInteraction,
|
||||
): Discord.TextChannel[] {
|
||||
const channels = Array.from(Array(CHANNEL_OPTIONS_MAX).keys()).map((index) =>
|
||||
interaction.options.getChannel(`channel-${index + 1}`, index === 0)
|
||||
interaction.options.getChannel(`channel-${index + 1}`, index === 0),
|
||||
);
|
||||
const textChannels = channels.filter(
|
||||
(c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel
|
||||
(c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel,
|
||||
);
|
||||
return textChannels;
|
||||
}
|
||||
|
||||
function helpMessage(): AgnosticReplyOptions {
|
||||
const avatarURL = client.user.avatarURL() || undefined;
|
||||
const embed = new Discord.MessageEmbed()
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: client.user.username || packageJson().name,
|
||||
iconURL: avatarURL,
|
||||
})
|
||||
.setThumbnail(avatarURL as string)
|
||||
.setDescription(
|
||||
`A Markov chain chatbot that speaks based on learned messages from previous chat input.`
|
||||
)
|
||||
.addField(
|
||||
`${config.messageCommandPrefix} or /${messageCommand.name}`,
|
||||
`Generates a sentence to say based on the chat database. Send your message as TTS to recieve it as TTS.`
|
||||
)
|
||||
.addField(
|
||||
`/${listenChannelCommand.name}`,
|
||||
`Add, remove, list, or modify the list of channels the bot listens to.`
|
||||
)
|
||||
.addField(
|
||||
`${config.messageCommandPrefix} train or /${trainCommand.name}`,
|
||||
`Fetches the maximum amount of previous messages in the listened to text channels. This takes some time.`
|
||||
)
|
||||
.addField(
|
||||
`${config.messageCommandPrefix} invite or /${inviteCommand.name}`,
|
||||
`Post this bot's invite URL.`
|
||||
)
|
||||
.addField(
|
||||
`${config.messageCommandPrefix} debug or /${messageCommand.name} debug: True`,
|
||||
`Runs the ${config.messageCommandPrefix} command and follows it up with debug info.`
|
||||
)
|
||||
.addField(
|
||||
`${config.messageCommandPrefix} tts or /${messageCommand.name} tts: True`,
|
||||
`Runs the ${config.messageCommandPrefix} command and reads it with text-to-speech.`
|
||||
`A Markov chain chatbot that speaks based on learned messages from previous chat input.`,
|
||||
)
|
||||
.addFields([
|
||||
{
|
||||
name: `${config.messageCommandPrefix} or /${messageCommand.name}`,
|
||||
value: `Generates a sentence to say based on the chat database. Send your message as TTS to recieve it as TTS.`,
|
||||
},
|
||||
|
||||
{
|
||||
name: `/${listenChannelCommand.name}`,
|
||||
value: `Add, remove, list, or modify the list of channels the bot listens to.`,
|
||||
},
|
||||
|
||||
{
|
||||
name: `${config.messageCommandPrefix} train or /${trainCommand.name}`,
|
||||
value: `Fetches the maximum amount of previous messages in the listened to text channels. This takes some time.`,
|
||||
},
|
||||
|
||||
{
|
||||
name: `${config.messageCommandPrefix} invite or /${inviteCommand.name}`,
|
||||
value: `Post this bot's invite URL.`,
|
||||
},
|
||||
|
||||
{
|
||||
name: `${config.messageCommandPrefix} debug or /${messageCommand.name} debug: True`,
|
||||
value: `Runs the ${config.messageCommandPrefix} command and follows it up with debug info.`,
|
||||
},
|
||||
|
||||
{
|
||||
name: `${config.messageCommandPrefix} tts or /${messageCommand.name} tts: True`,
|
||||
value: `Runs the ${config.messageCommandPrefix} command and reads it with text-to-speech.`,
|
||||
},
|
||||
])
|
||||
.setFooter({
|
||||
text: `${packageJson().name} ${getVersion()} by ${
|
||||
(packageJson().author as PackageJsonPerson).name
|
||||
@@ -664,13 +692,13 @@ function helpMessage(): AgnosticReplyOptions {
|
||||
|
||||
function generateInviteUrl(): string {
|
||||
return client.generateInvite({
|
||||
scopes: ['bot', 'applications.commands'],
|
||||
scopes: [Discord.OAuth2Scopes.Bot, Discord.OAuth2Scopes.ApplicationsCommands],
|
||||
permissions: [
|
||||
'VIEW_CHANNEL',
|
||||
'SEND_MESSAGES',
|
||||
'SEND_TTS_MESSAGES',
|
||||
'ATTACH_FILES',
|
||||
'READ_MESSAGE_HISTORY',
|
||||
'ViewChannel',
|
||||
'SendMessages',
|
||||
'SendTTSMessages',
|
||||
'AttachFiles',
|
||||
'ReadMessageHistory',
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -678,16 +706,18 @@ function generateInviteUrl(): string {
|
||||
function inviteMessage(): AgnosticReplyOptions {
|
||||
const avatarURL = client.user.avatarURL() || undefined;
|
||||
const inviteUrl = generateInviteUrl();
|
||||
const embed = new Discord.MessageEmbed()
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setAuthor({ name: `Invite ${client.user?.username}`, iconURL: avatarURL })
|
||||
.setThumbnail(avatarURL as string)
|
||||
.addField('Invite', `[Invite ${client.user.username} to your server](${inviteUrl})`);
|
||||
.addFields([
|
||||
{ name: 'Invite', value: `[Invite ${client.user.username} to your server](${inviteUrl})` },
|
||||
]);
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
async function handleResponseMessage(
|
||||
generatedResponse: GenerateResponse,
|
||||
message: Discord.Message
|
||||
message: Discord.Message,
|
||||
): Promise<void> {
|
||||
if (generatedResponse.message) await message.reply(generatedResponse.message);
|
||||
if (generatedResponse.debug) await message.reply(generatedResponse.debug);
|
||||
@@ -696,7 +726,7 @@ async function handleResponseMessage(
|
||||
|
||||
async function handleUnprivileged(
|
||||
interaction: Discord.CommandInteraction | Discord.SelectMenuInteraction,
|
||||
deleteReply = true
|
||||
deleteReply = true,
|
||||
): Promise<void> {
|
||||
if (deleteReply) await interaction.deleteReply();
|
||||
await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true });
|
||||
@@ -704,7 +734,7 @@ async function handleUnprivileged(
|
||||
|
||||
async function handleNoGuild(
|
||||
interaction: Discord.CommandInteraction | Discord.SelectMenuInteraction,
|
||||
deleteReply = true
|
||||
deleteReply = true,
|
||||
): Promise<void> {
|
||||
if (deleteReply) await interaction.deleteReply();
|
||||
await interaction.followUp({ content: INVALID_GUILD_MESSAGE, ephemeral: true });
|
||||
@@ -820,7 +850,7 @@ client.on('threadDelete', async (thread) => {
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
client.on('interactionCreate', async (interaction) => {
|
||||
if (interaction.isCommand()) {
|
||||
if (interaction.isChatInputCommand()) {
|
||||
L.info({ command: interaction.commandName }, 'Recieved slash command');
|
||||
|
||||
if (interaction.commandName === helpCommand.name) {
|
||||
@@ -869,7 +899,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
const channels = getChannelsFromInteraction(interaction);
|
||||
await addValidChannels(channels, interaction.guildId);
|
||||
await interaction.editReply(
|
||||
`Added ${channels.length} text channels to the list. Use \`/train\` to update the past known messages.`
|
||||
`Added ${channels.length} text channels to the list. Use \`/train\` to update the past known messages.`,
|
||||
);
|
||||
} else if (subCommand === 'remove') {
|
||||
if (!isModerator(interaction.member)) {
|
||||
@@ -881,7 +911,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
const channels = getChannelsFromInteraction(interaction);
|
||||
await removeValidChannels(channels, interaction.guildId);
|
||||
await interaction.editReply(
|
||||
`Removed ${channels.length} text channels from the list. Use \`/train\` to remove these channels from the past known messages.`
|
||||
`Removed ${channels.length} text channels from the list. Use \`/train\` to remove these channels from the past known messages.`,
|
||||
);
|
||||
} else if (subCommand === 'modify') {
|
||||
if (!interaction.guild) {
|
||||
@@ -892,8 +922,8 @@ client.on('interactionCreate', async (interaction) => {
|
||||
}
|
||||
await interaction.deleteReply();
|
||||
const dbTextChannels = await getTextChannels(interaction.guild);
|
||||
const row = new Discord.MessageActionRow().addComponents(
|
||||
new Discord.MessageSelectMenu()
|
||||
const row = new Discord.ActionRowBuilder<Discord.StringSelectMenuBuilder>().addComponents(
|
||||
new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId('listen-modify-select')
|
||||
.setPlaceholder('Nothing selected')
|
||||
.setMinValues(0)
|
||||
@@ -903,8 +933,8 @@ client.on('interactionCreate', async (interaction) => {
|
||||
label: `#${c.name}` || c.id,
|
||||
value: c.id,
|
||||
default: c.listen || false,
|
||||
}))
|
||||
)
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
await interaction.followUp({
|
||||
@@ -919,7 +949,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
const trainingJSON = interaction.options.getAttachment('json');
|
||||
|
||||
if (trainingJSON) {
|
||||
const responseMessage = await trainFromAttachmentJson(trainingJSON, interaction, clean);
|
||||
const responseMessage = await trainFromAttachmentJson(trainingJSON.url, interaction, clean);
|
||||
await interaction.followUp(responseMessage);
|
||||
} else {
|
||||
const reply = (await interaction.fetchReply()) as Discord.Message; // Must fetch the reply ASAP
|
||||
@@ -928,7 +958,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
await reply.reply({ content: responseMessage });
|
||||
}
|
||||
}
|
||||
} else if (interaction.isSelectMenu()) {
|
||||
} else if (interaction.isStringSelectMenu()) {
|
||||
if (interaction.customId === 'listen-modify-select') {
|
||||
await interaction.deferUpdate();
|
||||
const { guild } = interaction;
|
||||
@@ -940,14 +970,15 @@ client.on('interactionCreate', async (interaction) => {
|
||||
}
|
||||
|
||||
const allChannels =
|
||||
(interaction.component as APISelectMenuComponent).options?.map((o) => o.value) || [];
|
||||
(interaction.component as Discord.StringSelectMenuComponent).options?.map((o) => o.value) ||
|
||||
[];
|
||||
const selectedChannelIds = interaction.values;
|
||||
|
||||
const textChannels = (
|
||||
await Promise.all(
|
||||
allChannels.map(async (c) => {
|
||||
return guild.channels.fetch(c);
|
||||
})
|
||||
}),
|
||||
)
|
||||
).filter((c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel);
|
||||
const unselectedChannels = textChannels.filter((t) => !selectedChannelIds.includes(t.id));
|
||||
|
||||
@@ -15,7 +15,7 @@ const logger = pino(
|
||||
},
|
||||
PinoPretty({
|
||||
translateTime: `SYS:standard`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
export default logger;
|
||||
|
||||
Reference in New Issue
Block a user