Compare commits

...

11 Commits

12 changed files with 282 additions and 12 deletions

View File

@ -4,7 +4,7 @@ import {
SlashCommandBuilder,
ChatInputCommandInteraction
} from 'discord.js';
import { VoiceChannel } from '../../../database.js';
import { Guild, VoiceChannel } from '../../../database.js';
export const data = new SlashCommandBuilder()
.setName('custom_vc')
@ -52,6 +52,7 @@ export async function execute(interaction) {
/** @type {string} */
let step;
const guildData = { id: guild.id };
try {
switch (options.getSubcommand()) {
case 'create': {
@ -65,10 +66,16 @@ export async function execute(interaction) {
type: ChannelType.GuildVoice
});
// Save channel data
step = 'save';
// Create guild if not exists
await Guild.findOrCreate({
where: guildData,
defaults: guildData
});
// Save channel data
await VoiceChannel.create({
id: channel.id,
guild: guild.id,
create: true
});
@ -85,9 +92,18 @@ export async function execute(interaction) {
// Get channel id from user input
const { id } = options.getChannel('channel');
// Save channel data
step = 'save';
await VoiceChannel.create({ id, create: true });
// Create guild if not exists
await Guild.findOrCreate({
where: guildData,
defaults: guildData
});
// Save channel data
await VoiceChannel.create({
id,
guild: guild.id,
create: true
});
// Reply success to acknowledge command
await interaction.reply({

View File

@ -0,0 +1,103 @@
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
import { Role, Guild } from '../../../database.js';
/**
* @param {Guild} guild
* @param {Role} role
*/
const registerRole = async (guild, role) => {
// Check if guild exists in database, otherwise create it
const guildData = { id: guild.id };
await Guild.findOrCreate({
where: guildData,
defaults: guildData
});
// Register role in database
await Role.create({
guild: guild.id,
id: role.id,
assign: true
});
};
export const data = new SlashCommandBuilder()
.setName('member_roles')
.setDMPermission(false)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
.setDescription('Assigns roles to new members.')
.addSubcommand((subcommand) =>
subcommand
.setName('add')
.setDescription('Registers a role to be assigned to new members.')
.addRoleOption((option) =>
option
.setName('role')
.setDescription('The role to assign to new members.')
.setRequired(true)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('remove')
.setDescription('Unregisters a role from new member assignment.')
.addRoleOption((option) =>
option
.setName('role')
.setDescription('The role to unregister from assignmment.')
.setRequired(true)
)
);
/** @param {ChatInputCommandInteraction} interaction */
export async function execute(interaction) {
const { options } = interaction;
// Get command options
const role = options.getRole('role');
switch (options.getSubcommand()) {
case 'add':
// Search for role in database
const found = await Role.findOne({
where: {
id: role.id
}
});
// Toggle role assignment if found
if (found) {
found.assign = true;
await found.save();
// Otherwise create new database entry
} else await registerRole(interaction.guild, role);
// Reply successfully to acknowledge command
await interaction.reply({
content: 'Successfully registered role.',
ephemeral: true
});
console.info(`[INFO] Registered role to be assigned with ID '${role.id}'.`);
break;
case 'remove':
// Remove role from database
const count = await Role.destroy({
where: {
id: role.id,
assign: true
}
});
// Set reply based on result of deletion
let response = 'Successfully removed';
if (count === 0) response = 'Failed to remove';
// Reply to acknowledge command
await interaction.reply({
content: `${response} role from new member assignment!`,
ephemeral: true
});
console.info(`[INFO] Removed role to be assigned with ID '${role.id}'.`);
break;
}
}

View File

@ -1,6 +1,6 @@
import { PermissionFlagsBits, SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { addSelfRoles, removeSelfRoles } from '../../../shared.js';
import { Message } from '../../../database.js';
import { Guild, Message } from '../../../database.js';
/**
* Sends a `Message` in the current channel and registers for self roles.
@ -46,6 +46,13 @@ const registerSelfRoles = async (interaction) => {
// Get message by id
await channel.messages.fetch(id);
// Check if message is already registered
const found = await Message.findOne({
where: { id }
});
if (found) throw new Error('Message already registered!');
// Reply successfully to acknowledge command
await interaction.reply({
content: 'Successfully fetched message!',
@ -186,8 +193,18 @@ export async function execute(interaction) {
if (createNew) {
try {
// Create guild if not exists
const guildData = { id: interaction.guild.id };
await Guild.findOrCreate({
where: guildData,
defaults: guildData
});
// Create database entry
await Message.create({ id });
await Message.create({
id,
guild: interaction.guild.id
});
} catch (error) {
console.error(error);

View File

@ -1,6 +1,8 @@
import defineRoleEmojiPair from './models/roleEmojiPairs.js';
import defineVoiceChannel from './models/voiceChannels.js';
import defineMessage from './models/messages.js';
import defineGuild from './models/guilds.js';
import defineRole from './models/roles.js';
import { Sequelize } from 'sequelize';
import { config } from 'dotenv';
@ -21,5 +23,13 @@ const VoiceChannel = defineVoiceChannel(sequelize);
const Message = defineMessage(sequelize);
Message.hasMany(RoleEmojiPair, { foreignKey: 'message', onDelete: 'CASCADE' });
const Role = defineRole(sequelize);
Role.hasMany(RoleEmojiPair, { foreignKey: 'role', onDelete: 'CASCADE' });
const Guild = defineGuild(sequelize);
Guild.hasMany(VoiceChannel, { foreignKey: 'guild', onDelete: 'CASCADE' });
Guild.hasMany(Message, { foreignKey: 'guild', onDelete: 'CASCADE' });
Guild.hasMany(Role, { foreignKey: 'guild', onDelete: 'CASCADE' });
sequelize.sync();
export { sequelize, RoleEmojiPair, VoiceChannel, Message };
export { sequelize, Guild, Role, RoleEmojiPair, VoiceChannel, Message };

View File

@ -0,0 +1,27 @@
import { Events, GuildMember } from 'discord.js';
import { Role } from '../../database.js';
export const name = Events.GuildMemberAdd;
/** @param {GuildMember} member */
export async function execute(member) {
// Find roles to be assigned in guild from database
const roles = await Role.findAll({
where: {
guild: member.guild.id,
assign: true
}
});
// Ignore if no none found
if (roles.length === 0) return;
try {
// Add roles to member
await member.roles.add(roles.map((role) => role.id));
} catch (error) {
// Missing permissions
console.error(error);
await member.user.send('Could not assign roles. Please contact server staff.');
}
console.info(`[INFO] Added ${roles.length} roles to new member with ID '${member.user.id}'.`);
}

View File

@ -17,6 +17,7 @@ const runClient = (commands, events) => {
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessageReactions

24
models/guilds.js Normal file
View File

@ -0,0 +1,24 @@
import { DataTypes, Sequelize } from 'sequelize';
/**
* @typedef {Object} Guild
* @property {string} id A Discord guild ID.
* @property {(model: Object) => void} hasMany Defines an One-To-Many relationship.
* @property {(conditions: Object) => Promise<Guild>} findOne Finds one instance in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Array<Guild>>} findAll Finds all instances in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Guild>} findOrCreate Finds or creates an instance in the database matching the provided condition(-s) or default values.
*/
/**
* The definition of the `Guild` table in the database.
* @param {Sequelize} sequelize
* @returns {Guild}
*/
export default function (sequelize) {
return sequelize.define('Guilds', {
id: {
type: DataTypes.STRING,
primaryKey: true
}
});
}

View File

@ -1,11 +1,13 @@
import { DataTypes, Sequelize } from 'sequelize';
import { DataTypes, Deferrable, Sequelize } from 'sequelize';
/**
* @typedef {Object} Message
* @property {string} id A Discord message ID.
* @property {string} guild A Discord guild ID as a foreign key reference.
* @property {(model: Object) => void} hasMany Defines an One-To-Many relationship.
* @property {(conditions: Object) => Promise<Message>} findOne Finds one instance in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Array<Message>>} findAll Finds all instances in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Message>} findOrCreate Finds or creates an instance in the database matching the provided condition(-s) or default values.
*/
/**
@ -18,6 +20,14 @@ export default function (sequelize) {
id: {
type: DataTypes.STRING,
primaryKey: true
},
guild: {
type: DataTypes.STRING,
references: {
deferrable: Deferrable.INITIALLY_IMMEDIATE,
model: 'Guilds',
key: 'id'
}
}
});
}

View File

@ -9,6 +9,7 @@ import { DataTypes, Deferrable, Sequelize } from 'sequelize';
* @property {(model: Object) => void} hasMany Defines an One-To-Many relationship.
* @property {(conditions: Object) => Promise<RoleEmojiPair>} findOne Finds one instance in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Array<RoleEmojiPair>>} findAll Finds all instances in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<RoleEmojiPair>} findOrCreate Finds or creates an instance in the database matching the provided condition(-s) or default values.
*/
/**
@ -32,7 +33,12 @@ export default function (sequelize) {
}
},
role: {
type: DataTypes.STRING
type: DataTypes.STRING,
references: {
deferrable: Deferrable.INITIALLY_IMMEDIATE,
model: 'Roles',
key: 'id'
}
},
emoji: {
type: DataTypes.STRING

38
models/roles.js Normal file
View File

@ -0,0 +1,38 @@
import { DataTypes, Deferrable, Sequelize } from 'sequelize';
/**
* @typedef {Object} Role
* @property {string} id A Discord role ID.
* @property {boolean} assign Whether or not the role should be assigned to new members.
* @property {string} guild A Discord guild ID as a foreign key reference.
* @property {(model: Object) => void} hasMany Defines an One-To-Many relationship.
* @property {(conditions: Object) => Promise<Role>} findOne Finds one instance in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Array<Role>>} findAll Finds all instances in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Role>} findOrCreate Finds or creates an instance in the database matching the provided condition(-s) or default values.
*/
/**
* The definition of the `Role` table in the database.
* @param {Sequelize} sequelize
* @returns {Role}
*/
export default function (sequelize) {
return sequelize.define('Roles', {
id: {
type: DataTypes.STRING,
primaryKey: true
},
assign: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
guild: {
type: DataTypes.STRING,
references: {
deferrable: Deferrable.INITIALLY_IMMEDIATE,
model: 'Guilds',
key: 'id'
}
}
});
}

View File

@ -1,13 +1,15 @@
import { DataTypes, Sequelize } from 'sequelize';
import { DataTypes, Deferrable, Sequelize } from 'sequelize';
/**
* @typedef {Object} VoiceChannel
* @property {string} id A Discord channel ID.
* @property {boolean} create Whether or not this channel is registered to create customs when joined.
* @property {(string|null)} owner The owner of this channel, if not registered for customs.
* @property {string} guild A Discord guild ID as a foreign key reference.
* @property {(model: Object) => void} hasMany Defines an One-To-Many relationship.
* @property {(conditions: Object) => Promise<VoiceChannel>} findOne Finds one instance in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<Array<VoiceChannel>>} findAll Finds all instances in the database matching the provided condition(-s).
* @property {(conditions: Object) => Promise<VoiceChannel>} findOrCreate Finds or creates an instance in the database matching the provided condition(-s) or default values.
*/
/**
@ -16,7 +18,7 @@ import { DataTypes, Sequelize } from 'sequelize';
* @returns {VoiceChannel}
*/
export default function (sequelize) {
return sequelize.define('VoiceChannel', {
return sequelize.define('VoiceChannels', {
id: {
type: DataTypes.STRING,
primaryKey: true
@ -28,6 +30,14 @@ export default function (sequelize) {
owner: {
type: DataTypes.STRING,
allowNull: true
},
guild: {
type: DataTypes.STRING,
references: {
deferrable: Deferrable.INITIALLY_IMMEDIATE,
model: 'Guilds',
key: 'id'
}
}
});
}

View File

@ -65,10 +65,18 @@ const saveMessageData = async (id, role, emoji) => {
`Existing RoleEmojiPair entry with (partial) data {message:${id},role:${role.id},emoji:${emoji}}!`
);
// Create guild if not exists
const guildData = { id: role.guild.id };
await Guild.findOrCreate({
where: guildData,
defaults: guildData
});
// Create database entry for pair
await RoleEmojiPair.create({
message: id,
role: role.id,
guild: guildData.id,
emoji: emoji.replace(/:(\s*[^:]*\s*):/, ':_:')
});
};
@ -146,7 +154,7 @@ const optional = ['autocomplete', 'modalSubmit'];
/**
* Recursively scans a directory for all files in it.
* @param {string} dir
* @returns {Array<string>} Array of paths to the files within.
* @returns {Promise<Array<string>>} Array of paths to the files within.
*/
export const getFiles = async (dir) => {
const dirents = await readdir(dir, { withFileTypes: true });