initial commit

This commit is contained in:
2024-10-13 14:50:19 +00:00
commit dbdbf16bfe
34 changed files with 3035 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
import discord, {
CommandInteraction,
CommandInteractionOptionResolver,
SlashCommandBuilder,
SlashCommandRoleOption,
SlashCommandStringOption,
SlashCommandIntegerOption,
SlashCommandSubcommandBuilder,
EmbedBuilder,
InteractionResponse,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import {HydratedDocument} from 'mongoose';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {config} from '../../config';
import {Guild, guildModel} from '../../models/Guild';
import {GiveawayMsg, giveawayMsgModel} from '../../models/GiveawayMsg';
type CIOR = CommandInteractionOptionResolver;
async function add(interaction: CommandInteraction): Promise<void>{
const message: string | null =
(interaction.options as CIOR).getString('message');
if(!message)
throw Error('message not exist');
const embed = new EmbedBuilder()
.setTitle('New Giveaway')
.setDescription(`@everyone\n${message}`);
if(!interaction.guildId)
throw Error('guildId not exist');
if(!interaction.channelId)
throw Error('channelId not exist');
let msg: discord.Message =
await (await interaction.reply({embeds: [embed]})).fetch();
await msg.react(config.giveaway.emoji);
if(!interaction.guild || !interaction.guild.id)
throw Error('guild not exist');
const guild: HydratedDocument<Guild> | null =
await guildModel.findOneAndUpdate(
{id: interaction.guild.id},
{$set: {name: interaction.guild.name}},
{upsert: true, new: true}
);
if(!guild)
throw Error('guild not exist');
const giveawayMsg: HydratedDocument<GiveawayMsg> | null =
await giveawayMsgModel.findOneAndUpdate(
{guild: guild, messageId: msg.id, authorId: interaction.user.id},
{}, {upsert: true, new: true}
);
if(!giveawayMsg)
throw Error('giveawayMsg not exist');
await msg.edit({embeds: [new EmbedBuilder()
.setColor(Math.floor(Math.random()*16777215))
.setTitle('Giveaway')
.setDescription(message)
.setFooter({text: `id: ${msg.id}`})
]});
}
async function pick(interaction: CommandInteraction): Promise<void>{
const messageId: string | null =
(interaction.options as CIOR).getString('message');
if(!messageId)
throw Error('message not exist');
const num: number | null =
(interaction.options as CIOR).getInteger('number');
if(!num)
throw Error('number not exist');
const dbGiveawayMsg: HydratedDocument<GiveawayMsg> | null =
await giveawayMsgModel.findOneAndDelete({
messageId: messageId, authorId: interaction.user.id
});
if(!dbGiveawayMsg){
await interaction.reply({
content: logger.log(`Giveaway didn't exist or the author didn't match.`),
ephemeral: true
});
return;
}
for(let i = dbGiveawayMsg.userIds.length - 1; i > 0; i--){
let j = Math.floor(Math.random() * (i + 1));
[dbGiveawayMsg.userIds[i], dbGiveawayMsg.userIds[j]]
= [dbGiveawayMsg.userIds[j], dbGiveawayMsg.userIds[i]];
}
let content: string = '### Giveaway Result\n';
for(let i = 0; i < num; i++)
content += `1. ${await interaction.client.users.fetch(dbGiveawayMsg.userIds[i])}\n`;
content += `will obtain the prize for giveaway, id: ${dbGiveawayMsg.messageId}`;
await interaction.reply({content: content});
}
class Giveaway extends Command{
get name(){return 'giveaway';}
get description(){return 'Setup a Giveaway message.';}
async execute(interaction: CommandInteraction): Promise<void>{
try{
const subcommand: string | null =
(interaction.options as CIOR).getSubcommand();
switch(subcommand){
case 'add':
await add(interaction); break;
case 'pick':
await pick(interaction); break;
default:
throw Error('subcommand not exist');
}
}catch(err: unknown){
let message;
if(err instanceof Error) message = err.message;
else message = String(message);
logger.error(`While executing "/giveaway", ${message}`);
await interaction.reply({content: `While executing "/giveaway", ${message}`});
}
}
override build(): SlashCommandSubcommandsOnlyBuilder{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('add')
.setDescription('Construct a giveaway')
.addStringOption((option: SlashCommandStringOption) => option
.setName('message')
.setDescription('The message displayed with the giveaway message.')
.setRequired(true)
)
)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('pick')
.setDescription('Random pick a number of user in a giveaway')
.addStringOption((option: SlashCommandStringOption) => option
.setName('message')
.setDescription('The message id of the giveaway (Shown in the footer)')
.setRequired(true)
)
.addIntegerOption((option: SlashCommandIntegerOption) => option
.setName('number')
.setDescription('The number of member to choose.')
.setRequired(true)
)
)
}
};
export const command = new Giveaway();

221
commands/images/image.ts Normal file
View File

@@ -0,0 +1,221 @@
import discord, {
CommandInteraction,
CommandInteractionOptionResolver,
SlashCommandBuilder,
SlashCommandStringOption,
SlashCommandAttachmentOption,
SlashCommandSubcommandBuilder,
EmbedBuilder,
InteractionResponse,
SlashCommandSubcommandsOnlyBuilder,
Attachment
} from 'discord.js';
import { HydratedDocument } from 'mongoose';
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { Client } from 'minio';
import { Command } from '../../classes/command';
import { logger } from '../../logger';
import { config } from '../../config';
import { reactPreprocess } from '../../functions/react-preprocess';
import { Image, imageModel } from '../../models/Image';
import { Alias, aliasModel } from '../../models/Alias';
import { Guild, guildModel } from '../../models/Guild';
import { Token, tokenModel } from '../../models/Token';
type CIOR = CommandInteractionOptionResolver;
const client = new Client(config.minio.client);
async function download(url: string, file: string): Promise<void>{
const saveFile = await axios({
url: url, method: 'GET', responseType: 'arraybuffer'
});
if(!await client.bucketExists(config.minio.bucket))
await client.makeBucket(config.minio.bucket);
await client.putObject(config.minio.bucket, file, saveFile.data);
}
async function upload(interaction: CommandInteraction): Promise<void>{
if(!interaction.guildId)
throw Error('guildId not exist');
const file: discord.Attachment | null =
(interaction.options as CIOR).getAttachment('file');
if(!file)
throw Error('file not exist');
const fileExt: string = path.extname(file.name);
const image: HydratedDocument<Image> | null =
await (new imageModel({extension: fileExt})).save();
if(!image)
throw Error('image not exist');
await download(file.url, `${String(image._id)}${fileExt}`);
const publicUrl: string = `${config.httpServer.external.url}/${String(image._id)}${fileExt}`;
await interaction.reply({
content: logger.log(`Image saved with id: ${image._id}.`),
});
if(interaction.channel)
await interaction.channel.send({content: publicUrl});
}
async function link(interaction: CommandInteraction): Promise<void>{
if(!interaction.guildId)
throw Error('guildId not exist');
let aliasText: string | null =
(interaction.options as CIOR).getString('alias');
if(!aliasText)
throw Error('aliasText not exist');
const imageId: string | null =
(interaction.options as CIOR).getString('image');
if(!imageId)
throw Error('imageId not exist');
aliasText = reactPreprocess(aliasText);
if(!interaction.guild || !interaction.guild.id || !interaction.guild.name)
throw Error('guild not exist');
const guild: HydratedDocument<Guild> | null =
await guildModel.findOneAndUpdate(
{id: interaction.guild.id},
{$set: {name: interaction.guild.name}},
{new: true, upsert: true}
);
if(!guild)
throw Error('guild not exist');
const image: HydratedDocument<Image> | null =
await imageModel.findById(imageId);
if(!image)
throw Error('image not exist');
const alias: HydratedDocument<Alias> | null =
await aliasModel.findOneAndUpdate(
{guild: guild, text: aliasText, images: {$nin: image}},
{$push: {images: image}},
{new: true, upsert: true}
);
if(!alias)
throw Error('alias update failed');
await interaction.reply({
content: logger.log(`Alias ${aliasText} has linked to image id: ${imageId}.`)
});
}
async function unlink(interaction: CommandInteraction): Promise<void>{
if(!interaction.guildId)
throw Error('guildId not exist');
let aliasText: string | null =
(interaction.options as CIOR).getString('alias');
if(!aliasText)
throw Error('aliasText not exist');
aliasText = reactPreprocess(aliasText);
if(!interaction.guild || !interaction.guild.id)
throw Error('guild not exist');
const guild: HydratedDocument<Guild> | null =
await guildModel.findOne({id: interaction.guild.id});
if(!guild)
throw Error('guild not exist');
const res = await aliasModel.deleteOne(
{guild: guild, text: aliasText}
);
if(!res)
logger.warning('alias not exist');
await interaction.reply({
content: logger.log(`All the image have been unlinked to alias ${aliasText}.`)
});
}
async function list(interaction: CommandInteraction): Promise<void>{
const token: string = String(Math.floor(Math.random() * 100000000));
if(!interaction.guild || !interaction.guild.id)
throw Error('guild not exist');
await tokenModel.create({
token: token,
guildId: interaction.guild.id,
exp: Date.now() + config.tokenTimeLimit
});
const embed = new EmbedBuilder()
.setColor(0x1f1e33)
.setTitle('Autoreact Image List')
.setDescription('experimental function')
.addFields({
name: 'One Time Link:',
value: `[open in browser](https://amane.konchin.com/auth?token=${token})`
});
await interaction.reply({embeds: [embed], ephemeral: true});
}
class ImageCmd extends Command{
get name(){return "image";}
get description(){return "Modify the reacting image database.";}
async execute(interaction: CommandInteraction): Promise<void>{
try{
const subcommand: string | null =
(interaction.options as CIOR).getSubcommand();
switch(subcommand){
case 'upload':
await upload(interaction); break;
case 'link':
await link(interaction); break;
case 'unlink':
await unlink(interaction); break;
case 'list':
await list(interaction); break;
default:
throw Error('subcommand not exist');
}
}catch(err: unknown){
let message;
if(err instanceof Error) message = err.message;
else message = String(message);
logger.error(`While executing "/image", ${message}`);
await interaction.reply({content: `While executing "/image", ${message}`});
}
}
override build(): SlashCommandSubcommandsOnlyBuilder{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('upload')
.setDescription('Upload an image.')
.addAttachmentOption((option: SlashCommandAttachmentOption) => option
.setName('file')
.setDescription('The image file that you want to upload.')
.setRequired(true)
)
)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('link')
.setDescription('Link an alias to an image')
.addStringOption((option: SlashCommandStringOption) => option
.setName('alias')
.setDescription('The alias to be linked.')
.setRequired(true)
)
.addStringOption((option: SlashCommandStringOption) => option
.setName('image')
.setDescription('The image to be linked, as id.')
.setRequired(true)
)
)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('unlink')
.setDescription('Unlink an alias to an image')
.addStringOption((option: SlashCommandStringOption) => option
.setName('alias')
.setDescription('The alias to be unlinked.')
.setRequired(true)
)
)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('list')
.setDescription('Show the image list')
)
}
};
export const command = new ImageCmd();

View File

@@ -0,0 +1,90 @@
import discord, {
CommandInteraction,
CommandInteractionOptionResolver,
SlashCommandBuilder,
SlashCommandRoleOption,
SlashCommandStringOption,
InteractionResponse,
} from 'discord.js';
import {HydratedDocument} from 'mongoose';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {AutoroleMsg, autoroleMsgModel} from '../../models/AutoroleMsg';
import {Guild, guildModel} from '../../models/Guild';
type CIOR = CommandInteractionOptionResolver;
class Autorole extends Command{
get name(){return "autorole";}
get description(){return "Setup a autorole message.";}
async execute(interaction: CommandInteraction): Promise<void>{
try{
let role = (interaction.options as CIOR).getRole('role');
if(!role)
throw Error('role not exist');
const title: string | null =
(interaction.options as CIOR).getString('title');
if(!title)
throw Error('title not exist');
const message: string | null =
(interaction.options as CIOR).getString('message');
if(!message)
throw Error('message not exist');
if(!interaction.guild || !interaction.guild.id || !interaction.guild.name)
throw Error('guildId not exist');
if(!interaction.channelId)
throw Error('channelId not exist');
let content: string = `>> **Auto Role: ${title}** <<\n`;
content += '*React to give yourself a role.*\n\n';
content += `${message}`;
let msg: discord.Message =
await (await interaction.reply({content: content})).fetch();
const guild: HydratedDocument<Guild> | null =
await guildModel.findOneAndUpdate(
{id: interaction.guild.id},
{$set: {name: interaction.guild.name}},
{upsert: true, new: true}
);
if(!guild)
throw Error('guild not exist');
const autoroleMsg: HydratedDocument<AutoroleMsg> | null =
await autoroleMsgModel.findOneAndUpdate(
{guild: guild, messageId: msg.id, roleId: role.id},
{}, {upsert: true, new: true}
);
if(!autoroleMsg)
throw Error('autoroleMsg not exist');
}catch(err: unknown){
let message;
if(err instanceof Error) message = err.message;
else message = String(message);
logger.error(`While executing "/autorole", ${message}`);
await interaction.reply({content: `While executing "/autorole", ${message}`});
}
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addRoleOption((option: SlashCommandRoleOption) => option
.setName('role')
.setDescription('The role that autorole gives when reacted.')
.setRequired(true)
)
.addStringOption((option: SlashCommandStringOption) => option
.setName('title')
.setDescription('The title displayed with the autorole message.')
.setRequired(true)
)
.addStringOption((option: SlashCommandStringOption) => option
.setName('message')
.setDescription('The additional message displayed with the autorole message.')
.setRequired(true)
)
}
};
export const command = new Autorole();

10
commands/tests/test.ts Normal file
View File

@@ -0,0 +1,10 @@
import {CommandInteraction} from 'discord.js';
import {Command} from '../../classes/command';
class Test extends Command{
get name(){return "test";}
get description(){return "Testing some features.";}
async execute(interaction: CommandInteraction): Promise<void>{}
};
// export const command = new Test();

29
commands/utils/help.ts Normal file
View File

@@ -0,0 +1,29 @@
import {CommandInteraction, EmbedBuilder} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {config} from '../../config';
class Help extends Command{
get name(){return "help";}
get description(){return "Help messages.";}
async execute(interaction: CommandInteraction): Promise<void>{
const embed = new EmbedBuilder()
.setTitle("Help message")
.setDescription(`Report issues on [this page](${config.urls.issue})`)
.setColor(0x1f1e33)
.setAuthor({
name: 'Ian Shih (konchin)',
url: config.urls.author,
iconURL: config.urls.icon
})
.setFields(
{name: 'Read the documentation', value: config.urls.help},
{name: 'Read the source code', value: config.urls.git}
);
await interaction.reply({embeds: [embed], ephemeral: true});
logger.log(`Command: help`);
}
};
export const command = new Help();

19
commands/utils/ping.ts Normal file
View File

@@ -0,0 +1,19 @@
import {CommandInteraction} from 'discord.js';
import {Command} from '../../classes/command';
class Ping extends Command{
get name(){return "ping";}
get description(){return "Reply with the RTT of this bot.";}
async execute(interaction: CommandInteraction): Promise<void>{
const sent = await interaction.reply({
content: "Pinging...",
ephemeral: true,
fetchReply: true,
});
await interaction.editReply(`Roundtrip latency: ${
sent.createdTimestamp - interaction.createdTimestamp
}ms`);
}
};
export const command = new Ping();

104
commands/utils/set.ts Normal file
View File

@@ -0,0 +1,104 @@
import discord, {
CommandInteraction,
CommandInteractionOptionResolver,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandStringOption,
SlashCommandChannelOption,
InteractionResponse,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import {HydratedDocument} from 'mongoose';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {Guild, guildModel} from '../../models/Guild';
type CIOR = CommandInteractionOptionResolver;
async function log(interaction: CommandInteraction): Promise<void>{
const feature: string | null =
(interaction.options as CIOR).getString('feature');
if(!feature)
throw Error('feature not exist');
const logChannel = (interaction.options as CIOR).getChannel('channel');
if(!logChannel || !('send' in logChannel))
throw Error('logChannel not exist or illegal');
if(!interaction.guild || !interaction.guild.id || !interaction.guild.name)
throw Error('guild not exist');
let guild: HydratedDocument<Guild> | null = null;
switch(feature){
case 'giveaway':
await guildModel.findOneAndUpdate(
{id: interaction.guild.id},
{$set: {
name: interaction.guild.name,
giveawayLogChannelId: logChannel.id
}},
{new: true, upsert: true}
);
break;
case 'autorole':
await guildModel.findOneAndUpdate(
{id: interaction.guild.id},
{$set: {
name: interaction.guild.name,
autoroleLogChannelId: logChannel.id
}},
{new: true, upsert: true}
);
break;
}
await interaction.reply({
content: logger.log(`The log channel of ${feature} has been set to ${logChannel}`)
});
;
}
class SetVariables extends Command{
get name(){return "set";}
get description(){return "Set guild variables.";}
async execute(interaction: CommandInteraction): Promise<void>{
try{
const subcommand: string =
(interaction.options as CIOR).getSubcommand();
switch(subcommand){
case 'log':
await log(interaction); break;
default:
throw Error('subcommand not exist');
}
}catch(err: unknown){
let message;
if(err instanceof Error) message = err.message;
else message = String(message);
logger.error(`While executing "/set", ${message}`);
await interaction.reply({content: `While executing "/set", ${message}`});
}
}
override build(): SlashCommandSubcommandsOnlyBuilder{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) => subcommand
.setName('log')
.setDescription('Set log of features.')
.addStringOption((option: SlashCommandStringOption) => option
.setName('feature')
.setDescription('Specify the to be set feature.')
.setRequired(true)
.addChoices(
{name: 'giveaway', value: 'giveaway'},
{name: 'autorole', value: 'autorole'},
)
)
.addChannelOption((option: SlashCommandChannelOption) => option
.setName('channel')
.setDescription('The channel that log send to.')
.setRequired(true)
)
)
}
};
export const command = new SetVariables();