mirror of
https://github.com/pacnpal/markov-discord.git
synced 2025-12-20 03:01:04 -05:00
Complete channel add/remove/list functionality
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -2669,7 +2669,7 @@
|
|||||||
"node_modules/markov-strings-db": {
|
"node_modules/markov-strings-db": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "file:../markov-strings/markov-strings-db-4.0.0.tgz",
|
"resolved": "file:../markov-strings/markov-strings-db-4.0.0.tgz",
|
||||||
"integrity": "sha512-CBYNkqUqj0XVohyBLz6kJL81VKzh+8xLcN6vp0ojps/AjqmycKHmj/xZWdCZjc72X7r85UaLnJ6L7QqnW+xPEw==",
|
"integrity": "sha512-AB1Sp0ukD+DpjeYFeiPhRgZXou6tUrmNn85dFBI2wAcCj2mzlolsTWV1zBhL0jmPtMoX7xwwf4FhDefMtY+E7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
@@ -6409,7 +6409,7 @@
|
|||||||
},
|
},
|
||||||
"markov-strings-db": {
|
"markov-strings-db": {
|
||||||
"version": "file:../markov-strings/markov-strings-db-4.0.0.tgz",
|
"version": "file:../markov-strings/markov-strings-db-4.0.0.tgz",
|
||||||
"integrity": "sha512-CBYNkqUqj0XVohyBLz6kJL81VKzh+8xLcN6vp0ojps/AjqmycKHmj/xZWdCZjc72X7r85UaLnJ6L7QqnW+xPEw==",
|
"integrity": "sha512-AB1Sp0ukD+DpjeYFeiPhRgZXou6tUrmNn85dFBI2wAcCj2mzlolsTWV1zBhL0jmPtMoX7xwwf4FhDefMtY+E7A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"typeorm": "^0.2.41"
|
"typeorm": "^0.2.41"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChannelType, Routes } from 'discord-api-types/v9';
|
|||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import { packageJson } from './util';
|
import { packageJson } from './util';
|
||||||
|
|
||||||
const CHANNEL_OPTIONS_MAX = 25;
|
export const CHANNEL_OPTIONS_MAX = 25;
|
||||||
|
|
||||||
export const helpCommand = new SlashCommandBuilder()
|
export const helpCommand = new SlashCommandBuilder()
|
||||||
.setName('help')
|
.setName('help')
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Guild } from './Guild';
|
|||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Channel extends BaseEntity {
|
export class Channel extends BaseEntity {
|
||||||
@PrimaryColumn()
|
@PrimaryColumn({ type: 'text' })
|
||||||
id: number;
|
id: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
default: false,
|
default: false,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Channel } from './Channel';
|
|||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Guild extends BaseEntity {
|
export class Guild extends BaseEntity {
|
||||||
@PrimaryColumn()
|
@PrimaryColumn({ type: 'text' })
|
||||||
id: number;
|
id: string;
|
||||||
|
|
||||||
@OneToMany(() => Channel, (channel) => channel.guild, { onDelete: 'CASCADE', cascade: true })
|
@OneToMany(() => Channel, (channel) => channel.guild, { onDelete: 'CASCADE', cascade: true })
|
||||||
channels: Channel[];
|
channels: Channel[];
|
||||||
|
|||||||
152
src/index.ts
152
src/index.ts
@@ -17,6 +17,7 @@ import { Channel } from './entity/Channel';
|
|||||||
import { Guild } from './entity/Guild';
|
import { Guild } from './entity/Guild';
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import {
|
import {
|
||||||
|
CHANNEL_OPTIONS_MAX,
|
||||||
deployCommands,
|
deployCommands,
|
||||||
helpCommand,
|
helpCommand,
|
||||||
inviteCommand,
|
inviteCommand,
|
||||||
@@ -30,6 +31,9 @@ interface MarkovDataCustom {
|
|||||||
attachments: string[];
|
attachments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 client = new Discord.Client<true>({
|
const client = new Discord.Client<true>({
|
||||||
intents: [Discord.Intents.FLAGS.GUILD_MESSAGES, Discord.Intents.FLAGS.GUILDS],
|
intents: [Discord.Intents.FLAGS.GUILD_MESSAGES, Discord.Intents.FLAGS.GUILDS],
|
||||||
presence: {
|
presence: {
|
||||||
@@ -54,47 +58,46 @@ const markovGenerateOptions: MarkovGenerateOptions<MarkovDataCustom> = {
|
|||||||
maxTries: config.maxTries,
|
maxTries: config.maxTries,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* #v3-complete
|
|
||||||
*/
|
|
||||||
async function getMarkovByGuildId(guildId: string): Promise<Markov> {
|
async function getMarkovByGuildId(guildId: string): Promise<Markov> {
|
||||||
const id = parseInt(guildId, 10);
|
const markov = new Markov({ id: guildId, options: { ...markovOpts, id: guildId } });
|
||||||
const markov = new Markov({ id, options: markovOpts });
|
|
||||||
await markov.setup(); // Connect the markov instance to the DB to assign it an ID
|
await markov.setup(); // Connect the markov instance to the DB to assign it an ID
|
||||||
return markov;
|
return markov;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function getValidChannels(guild: Discord.Guild): Promise<Discord.TextChannel[]> {
|
||||||
* #v3-complete
|
const dbChannels = await Channel.find({ guild: Guild.create({ id: guild.id }), listen: true });
|
||||||
*/
|
|
||||||
async function isValidChannel(channelId: string): Promise<boolean> {
|
|
||||||
const id = parseInt(channelId, 10);
|
|
||||||
const channel = await Channel.findOne(id);
|
|
||||||
if (!channel) {
|
|
||||||
L.warn({ channelId }, 'Channel does not exist, setting to valid');
|
|
||||||
await Channel.create({ id }).save();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return channel.listen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* #v3-complete
|
|
||||||
*/
|
|
||||||
async function getValidChannels(guildId: string): Promise<Discord.TextChannel[]> {
|
|
||||||
const id = parseInt(guildId, 10);
|
|
||||||
const dbChannels = await Channel.find({ guild: Guild.create({ id }), listen: true });
|
|
||||||
const channels = (
|
const channels = (
|
||||||
await Promise.all(dbChannels.map(async (dbc) => client.channels.fetch(dbc.id.toString())))
|
await Promise.all(
|
||||||
|
dbChannels.map(async (dbc) => {
|
||||||
|
return guild.channels.fetch(dbc.id.toString());
|
||||||
|
})
|
||||||
|
)
|
||||||
).filter((c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel);
|
).filter((c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel);
|
||||||
return channels;
|
return channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addValidChannels(channels: Discord.TextChannel[], guildId: string): Promise<void> {
|
||||||
|
const dbChannels = channels.map((c) => {
|
||||||
|
return Channel.create({ id: c.id, guild: Guild.create({ id: guildId }), listen: true });
|
||||||
|
});
|
||||||
|
await Channel.save(dbChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeValidChannels(
|
||||||
|
channels: Discord.TextChannel[],
|
||||||
|
guildId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const dbChannels = channels.map((c) => {
|
||||||
|
return Channel.create({ id: c.id, guild: Guild.create({ id: guildId }), listen: false });
|
||||||
|
});
|
||||||
|
await Channel.save(dbChannels);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the author of a message as moderator-like permissions.
|
* Checks if the author of a message as moderator-like permissions.
|
||||||
* @param {GuildMember} member Sender of the message
|
* @param {GuildMember} member Sender of the message
|
||||||
* @return {Boolean} True if the sender is a moderator.
|
* @return {Boolean} True if the sender is a moderator.
|
||||||
* #v3-complete
|
*
|
||||||
*/
|
*/
|
||||||
function isModerator(member: Discord.GuildMember | APIInteractionGuildMember | null): boolean {
|
function isModerator(member: Discord.GuildMember | APIInteractionGuildMember | null): boolean {
|
||||||
const MODERATOR_PERMISSIONS: Discord.PermissionResolvable[] = [
|
const MODERATOR_PERMISSIONS: Discord.PermissionResolvable[] = [
|
||||||
@@ -157,16 +160,14 @@ function messageToData(message: Discord.Message): AddDataProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively gets all messages in a text channel's history.
|
* Recursively gets all messages in a text channel's history.
|
||||||
* #v3-complete
|
|
||||||
*/
|
*/
|
||||||
async function saveGuildMessageHistory(
|
async function saveGuildMessageHistory(
|
||||||
interaction: Discord.Message | Discord.CommandInteraction
|
interaction: Discord.Message | Discord.CommandInteraction
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!isModerator(interaction.member as any))
|
if (!isModerator(interaction.member as any)) return INVALID_PERMISSIONS_MESSAGE;
|
||||||
return 'You do not have the permissions for this action.';
|
if (!interaction.guildId || !interaction.guild) return INVALID_GUILD_MESSAGE;
|
||||||
if (!interaction.guildId) return 'This action must be performed within a server.';
|
|
||||||
const markov = await getMarkovByGuildId(interaction.guildId);
|
const markov = await getMarkovByGuildId(interaction.guildId);
|
||||||
const channels = await getValidChannels(interaction.guildId);
|
const channels = await getValidChannels(interaction.guild);
|
||||||
|
|
||||||
if (!channels.length) {
|
if (!channels.length) {
|
||||||
L.warn({ guildId: interaction.guildId }, 'No channels to train from');
|
L.warn({ guildId: interaction.guildId }, 'No channels to train from');
|
||||||
@@ -212,6 +213,7 @@ async function saveGuildMessageHistory(
|
|||||||
interface GenerateResponse {
|
interface GenerateResponse {
|
||||||
message?: Discord.MessageOptions;
|
message?: Discord.MessageOptions;
|
||||||
debug?: Discord.MessageOptions;
|
debug?: Discord.MessageOptions;
|
||||||
|
error?: Discord.MessageOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,7 +222,6 @@ interface GenerateResponse {
|
|||||||
* @param debug Sends debug info as a message if true.
|
* @param debug Sends debug info as a message if true.
|
||||||
* @param tts If the message should be sent as TTS. Defaults to the TTS setting of the
|
* @param tts If the message should be sent as TTS. Defaults to the TTS setting of the
|
||||||
* invoking message.
|
* invoking message.
|
||||||
* #v3-complete
|
|
||||||
*/
|
*/
|
||||||
async function generateResponse(
|
async function generateResponse(
|
||||||
interaction: Discord.Message | Discord.CommandInteraction,
|
interaction: Discord.Message | Discord.CommandInteraction,
|
||||||
@@ -230,7 +231,7 @@ async function generateResponse(
|
|||||||
L.debug('Responding...');
|
L.debug('Responding...');
|
||||||
if (!interaction.guildId) {
|
if (!interaction.guildId) {
|
||||||
L.warn('Received an interaction without a guildId');
|
L.warn('Received an interaction without a guildId');
|
||||||
return { message: { content: 'This action must be performed within a server.' } };
|
return { message: { content: INVALID_GUILD_MESSAGE } };
|
||||||
}
|
}
|
||||||
if (!interaction.channelId) {
|
if (!interaction.channelId) {
|
||||||
L.warn('Received an interaction without a channelId');
|
L.warn('Received an interaction without a channelId');
|
||||||
@@ -286,13 +287,31 @@ async function generateResponse(
|
|||||||
return responseMessages;
|
return responseMessages;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
L.error(err);
|
L.error(err);
|
||||||
if (debug) {
|
return { error: { content: `\n\`\`\`\nERROR: ${err}\n\`\`\`` } };
|
||||||
return { debug: { content: `\n\`\`\`\nERROR: ${err}\n\`\`\`` } };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listValidChannels(interaction: Discord.CommandInteraction): Promise<string> {
|
||||||
|
if (!interaction.guildId || !interaction.guild) return INVALID_GUILD_MESSAGE;
|
||||||
|
const channels = await getValidChannels(interaction.guild);
|
||||||
|
const channelText = channels.reduce((list, channel) => {
|
||||||
|
return `${list}\n • <#${channel.id}>`;
|
||||||
|
}, '');
|
||||||
|
return `This bot is currently listening and learning from ${channels.length} channel(s).${channelText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChannelsFromInteraction(
|
||||||
|
interaction: Discord.CommandInteraction
|
||||||
|
): Discord.TextChannel[] {
|
||||||
|
const channels = Array.from(Array(CHANNEL_OPTIONS_MAX).keys()).map((index) =>
|
||||||
|
interaction.options.getChannel(`channel-${index + 1}`, index === 0)
|
||||||
|
);
|
||||||
|
const textChannels = channels.filter(
|
||||||
|
(c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel
|
||||||
|
);
|
||||||
|
return textChannels;
|
||||||
|
}
|
||||||
|
|
||||||
function helpMessage(): Discord.MessageOptions {
|
function helpMessage(): Discord.MessageOptions {
|
||||||
const avatarURL = client.user.avatarURL() || undefined;
|
const avatarURL = client.user.avatarURL() || undefined;
|
||||||
const embed = new Discord.MessageEmbed()
|
const embed = new Discord.MessageEmbed()
|
||||||
@@ -345,12 +364,15 @@ client.on('ready', async (readyClient) => {
|
|||||||
|
|
||||||
await deployCommands(readyClient.user.id);
|
await deployCommands(readyClient.user.id);
|
||||||
|
|
||||||
const guildsToSave = readyClient.guilds
|
const guildsToSave = readyClient.guilds.valueOf().map((guild) => Guild.create({ id: guild.id }));
|
||||||
.valueOf()
|
|
||||||
.map((guild) => Guild.create({ id: parseInt(guild.id, 10) }));
|
|
||||||
await Guild.upsert(guildsToSave, ['id']);
|
await Guild.upsert(guildsToSave, ['id']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.on('guildCreate', async (guild) => {
|
||||||
|
L.info({ guildId: guild.id }, 'Adding new guild');
|
||||||
|
await Guild.upsert(Guild.create({ id: guild.id }), ['id']);
|
||||||
|
});
|
||||||
|
|
||||||
client.on('error', (err) => {
|
client.on('error', (err) => {
|
||||||
L.error(err);
|
L.error(err);
|
||||||
});
|
});
|
||||||
@@ -372,6 +394,7 @@ client.on('messageCreate', async (message) => {
|
|||||||
const generatedResponse = await generateResponse(message);
|
const generatedResponse = await generateResponse(message);
|
||||||
if (generatedResponse.message) await message.reply(generatedResponse.message);
|
if (generatedResponse.message) await message.reply(generatedResponse.message);
|
||||||
if (generatedResponse.debug) await message.reply(generatedResponse.debug);
|
if (generatedResponse.debug) await message.reply(generatedResponse.debug);
|
||||||
|
if (generatedResponse.error) await message.reply(generatedResponse.error);
|
||||||
}
|
}
|
||||||
if (command === 'tts') {
|
if (command === 'tts') {
|
||||||
await generateResponse(message, false, true);
|
await generateResponse(message, false, true);
|
||||||
@@ -380,8 +403,8 @@ client.on('messageCreate', async (message) => {
|
|||||||
await generateResponse(message, true);
|
await generateResponse(message, true);
|
||||||
}
|
}
|
||||||
if (command === null) {
|
if (command === null) {
|
||||||
L.debug('Listening...');
|
|
||||||
if (!message.author.bot) {
|
if (!message.author.bot) {
|
||||||
|
L.debug('Listening...');
|
||||||
const markov = await getMarkovByGuildId(message.channel.guildId);
|
const markov = await getMarkovByGuildId(message.channel.guildId);
|
||||||
await markov.addData([messageToData(message)]);
|
await markov.addData([messageToData(message)]);
|
||||||
|
|
||||||
@@ -392,9 +415,6 @@ client.on('messageCreate', async (message) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* #v3-complete
|
|
||||||
*/
|
|
||||||
client.on('messageDelete', async (message) => {
|
client.on('messageDelete', async (message) => {
|
||||||
if (message.author?.bot) return;
|
if (message.author?.bot) return;
|
||||||
L.info(`Deleting message ${message.id}`);
|
L.info(`Deleting message ${message.id}`);
|
||||||
@@ -405,9 +425,6 @@ client.on('messageDelete', async (message) => {
|
|||||||
await markov.removeData([message.content]);
|
await markov.removeData([message.content]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* #v3-complete
|
|
||||||
*/
|
|
||||||
client.on('messageUpdate', async (oldMessage, newMessage) => {
|
client.on('messageUpdate', async (oldMessage, newMessage) => {
|
||||||
if (oldMessage.author?.bot) return;
|
if (oldMessage.author?.bot) return;
|
||||||
L.info(`Editing message ${oldMessage.id}`);
|
L.info(`Editing message ${oldMessage.id}`);
|
||||||
@@ -422,7 +439,6 @@ client.on('messageUpdate', async (oldMessage, newMessage) => {
|
|||||||
client.on('interactionCreate', async (interaction) => {
|
client.on('interactionCreate', async (interaction) => {
|
||||||
if (!interaction.isCommand()) return;
|
if (!interaction.isCommand()) return;
|
||||||
|
|
||||||
// Unprivileged commands
|
|
||||||
if (interaction.commandName === helpCommand.name) {
|
if (interaction.commandName === helpCommand.name) {
|
||||||
await interaction.reply(helpMessage());
|
await interaction.reply(helpMessage());
|
||||||
} else if (interaction.commandName === inviteCommand.name) {
|
} else if (interaction.commandName === inviteCommand.name) {
|
||||||
@@ -433,12 +449,40 @@ client.on('interactionCreate', async (interaction) => {
|
|||||||
const debug = interaction.options.getBoolean('debug') || false;
|
const debug = interaction.options.getBoolean('debug') || false;
|
||||||
const generatedResponse = await generateResponse(interaction, debug, tts);
|
const generatedResponse = await generateResponse(interaction, debug, tts);
|
||||||
if (generatedResponse.message) await interaction.editReply(generatedResponse.message);
|
if (generatedResponse.message) await interaction.editReply(generatedResponse.message);
|
||||||
|
else await interaction.deleteReply();
|
||||||
if (generatedResponse.debug) await interaction.followUp(generatedResponse.debug);
|
if (generatedResponse.debug) await interaction.followUp(generatedResponse.debug);
|
||||||
if (!Object.keys(generatedResponse).length) await interaction.deleteReply();
|
if (generatedResponse.error) {
|
||||||
}
|
await interaction.followUp({ ...generatedResponse.error, ephemeral: true });
|
||||||
// Privileged commands
|
}
|
||||||
if (interaction.commandName === listenChannelCommand.name) {
|
} else if (interaction.commandName === listenChannelCommand.name) {
|
||||||
await interaction.reply('Pong!');
|
await interaction.deferReply();
|
||||||
|
const subCommand = interaction.options.getSubcommand(true) as 'add' | 'remove' | 'list';
|
||||||
|
if (subCommand === 'list') {
|
||||||
|
const reply = await listValidChannels(interaction);
|
||||||
|
await interaction.editReply(reply);
|
||||||
|
} else if (subCommand === 'add') {
|
||||||
|
if (!isModerator(interaction.member as any)) {
|
||||||
|
await interaction.deleteReply();
|
||||||
|
await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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.`
|
||||||
|
);
|
||||||
|
} else if (subCommand === 'remove') {
|
||||||
|
if (!isModerator(interaction.member as any)) {
|
||||||
|
await interaction.deleteReply();
|
||||||
|
await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (interaction.commandName === trainCommand.name) {
|
} else if (interaction.commandName === trainCommand.name) {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
const responseMessage = await saveGuildMessageHistory(interaction);
|
const responseMessage = await saveGuildMessageHistory(interaction);
|
||||||
|
|||||||
Reference in New Issue
Block a user