Merge branch 'dev' into renovate/eslint-8.x

This commit is contained in:
Axel Olausson Holtenäs 2022-06-20 13:27:12 +02:00 committed by GitHub
commit 1269b6e77d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
198 changed files with 2741 additions and 3160 deletions

View file

@ -1,5 +1,6 @@
# Custom Dictionary Words
Controlpanel
cooldown
cpgg
dagen
discordjs
@ -22,6 +23,7 @@ pino
Poäng
Profil
rando
Repliable
satta
senaste
Sifell

View file

@ -1,32 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: "bug"
assignees: "VermiumSifell"
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- Commit: [git rev-parse HEAD]
- Branch: [git branch --show-current]
**Additional context**
Add any other context about the problem here.

60
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View file

@ -0,0 +1,60 @@
name: 🐞 Bug
description: File a bug/issue
title: "[BUG]: <short_description_of_issue>"
labels: ["bug"]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. Run '...'
3. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Node**: 13.14.0
- **npm**: 7.6.3
- **xyter**: 7d02cf9
value: |
- OS: `lsb_release -d`
- Node: `node -v`
- npm: `npm -v`
- xyter: `git rev-parse --short HEAD`
render: markdown
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

View file

@ -0,0 +1,48 @@
name: 🤖 Request a new command
description: Suggest an command for this project
title: "[New Command]: /<command category> [command_group] <command_name> "
labels: ["enhancement", "new-command"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! We will use `/manage credits give` as an example for our placeholders.
- type: input
id: category
attributes:
label: Category
description: Where should we put this command?
placeholder: ex. manage
validations:
required: true
- type: input
id: group
attributes:
label: Group
description: Does this command belong to this group?
placeholder: ex. credits
- type: input
id: command
attributes:
label: Command
description: What should we call this command?
placeholder: ex. give
validations:
required: true
- type: textarea
id: description
attributes:
label: What should the command do?
description: Please tell us your concept, how it should work, and if there should be any additional features.
placeholder: "I would like to have a command to give users credits, that would make it easier for me to administrate credits! I would like it to add a specified amount of credits to specified user, without taking credits from the executer. Command should require Manage Guild permission. When successful, it should return something like: Added <amount> credits to <user>!"
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

7
.gitignore vendored
View file

@ -5,9 +5,10 @@ config.json
package-lock.json
**/config/*.ts
!**/config/index.ts
!**/config/example.*.ts
config/
# Build
build/
# Logs

View file

@ -4,7 +4,7 @@
<br>
</h1>
<h3 align=center>An multi-purpose bot built with <a href=https://github.com/discordjs/discord.js>discord.js</a></h3>
<h3 align=center>A privacy-focused bot built with <a href=https://github.com/discordjs/discord.js>discord.js</a></h3>
<div align=center>
@ -15,61 +15,7 @@
</div>
<p align="center">
<a href="#about">About</a>
<a href="#Features">Features</a>
<a href="https://xyter.zyner.org">Documentation</a>
<a href="https://github.com/ZynerOrg/xyter/blob/master/docs/INSTALLATION.md">Installation</a>
<a href="#license">License</a>
<a href="#credits">Credits</a>
</p>
## ❓ About
Xyter is an open source, multi-purpose Discord bot that is develoepd by students. You can invite it to your Discord server using [this](https://bot.zyner.org) link! It comes packaged with a variety of commands and a multitude of settings that can be tailored to your server's specific needs.
**To run this on Pterodactyl** please set startup command to `./node_modules/.bin/ts-node src/index.ts`!
If you liked this repository, feel free to leave a star ⭐ to help promote Xyter!
**For a more updated documentation visit [this site](https://xyter.zyner.org/)!**
## ❗ Features
**10+** commands and counting across **13** different categories!
- 💰 **Credits**: `balance`, `gift`, `top`, `work`, `give`, `take`, `set` and `transfer`!
- 💬 **Counters**: `view`, `add`, `remove`!
- 🔨 **Settings**: `guild credits`, `guild pterodactyl`, `guild points` and `user appearence`!
- 👑 **Profile**: `view`!
- 🖼 **Reputation**: `give`!
- 💰 **Shop**: `roles buy`, `roles cancel` and `pterodactyl`!
- ❔ **Utilities**: `lookup`, `about` and `stats`!
- **Full list** of commands: [here](https://github.com/ZynerOrg/xyter/blob/master/docs/COMMANDS.md).
Xyter also comes packed with a variety of features, such as:
- **Slash Commands**
- **Multi-language support**.
- And much more! There are over **5+** settings to tweak!
## 📝 To-Do
- Bug fixes
- Code optimisation
- New discord features
- Suggestions we deem very good.
Some more is available in issues
## 📖 License
Released under the [GPL-3.0 License](https://github.com/ZynerOrg/xyter/blob/master/LICENSE) license.
## 📜 Credits
- **[Vermium#9649](https://github.com/VermiumSifell)** - Founder, creator, hoster.
- **[Mastergamer433#5762](https://github.com/Mastergamer433)** - Work command for credits.
- Want to be on this list, aswell? - Check out the [Contributing page](https://github.com/ZynerOrg/xyter/blob/master/docs/CONTRIBUTING.md).

View file

@ -27,16 +27,16 @@
"email": "vermium@zyner.org"
},
"dependencies": {
"@discordjs/builders": "^0.13.0",
"@discordjs/rest": "^0.4.0",
"@discordjs/builders": "^0.15.0",
"@discordjs/rest": "^0.5.0",
"@types/i18next-fs-backend": "^1.1.2",
"axios": "^0.27.2",
"chance": "^1.1.8",
"common": "^0.2.5",
"crypto": "^1.0.1",
"discord-api-types": "^0.33.0",
"discord-api-types": "^0.34.0",
"discord.js": "^13.6.0",
"i18n": "^0.14.2",
"i18n": "^0.15.0",
"i18next": "^21.6.13",
"i18next-async-backend": "^2.0.0",
"i18next-fs-backend": "^1.1.4",
@ -63,8 +63,9 @@
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-prettier": "^4.0.0",
"husky": "8.0.1",
"jest": "28.0.0",
"lint-staged": "^12.3.7",
"jest": "28.1.1",
"lint-staged": "13.0.1",
"nodemon": "^2.0.16",
"prettier": "^2.6.0"
},
"lint-staged": {

View file

@ -1,3 +1,4 @@
{
"extends": ["config:base"]
"extends": ["config:base"],
"baseBranches": ["dev"]
}

View file

@ -8,4 +8,7 @@ export const guildId = "";
export const hosterName = "someone";
// Hoster Url
export const hosterUrl = "scheme://domain.tld";
export const hosterUrl = "https://xyter.zyner.org/customization/change-hoster";
// Winston log level
export const logLevel = "info";

View file

@ -1,22 +0,0 @@
// 3rd party dependencies
import mongoose from "mongoose";
// Dependencies
import logger from "@logger";
// Configuration
import { url } from "@config/database";
export default async () => {
await mongoose.connect(url).then(async (connection) => {
logger.info(`Connected to database: ${connection.connection.name}`);
});
mongoose.connection.on("error", async (error) => {
logger.error(`${error}`);
});
mongoose.connection.on("warn", async (warning) => {
logger.warn(warning);
});
};

View file

@ -1,20 +0,0 @@
// 3rd party dependencies
import { Guild } from "discord.js";
// Dependencies
import updatePresence from "@helpers/updatePresence";
import fetchGuild from "@helpers/fetchGuild";
import logger from "@logger";
export default {
async execute(guild: Guild) {
const { client } = guild;
logger?.silly(`Added to guild: ${guild.name} (${guild.id})`);
await fetchGuild(guild);
await updatePresence(client);
logger.silly(`guildCreate: ${guild}`);
},
};

View file

@ -1,20 +0,0 @@
// 3rd party dependencies
import { Guild } from "discord.js";
// Dependencies
import updatePresence from "@helpers/updatePresence";
import dropGuild from "@helpers/dropGuild";
import logger from "@logger";
export default {
async execute(guild: Guild) {
const { client } = guild;
logger?.silly(`Deleted from guild: ${guild.name} (${guild.id})`);
await dropGuild(guild);
await updatePresence(client);
logger.silly(`guildDelete: ${guild}`);
},
};

View file

@ -1,58 +0,0 @@
import logger from "@logger";
import { GuildMember, MessageEmbed, TextChannel } from "discord.js";
import guildSchema from "@schemas/guild";
import getEmbedConfig from "@helpers/getEmbedConfig";
export default {
execute: async (member: GuildMember) => {
const { footerText, footerIcon, successColor } = await getEmbedConfig(
member.guild
);
const guildData = await guildSchema.findOne({ guildId: member.guild.id });
const { client } = member;
if (guildData === null) return;
if (guildData.audits.status !== true) return;
if (!guildData.audits.channelId) return;
const channel = client.channels.cache.get(`${guildData.audits.channelId}`);
if (channel === null) return;
(channel as TextChannel)
.send({
embeds: [
new MessageEmbed()
.setColor(successColor)
.setAuthor({
name: "Member Joined",
iconURL: member.user.displayAvatarURL(),
})
.setDescription(`${member.user} ${member.user.tag}`)
.addFields([
{ name: "Account Age", value: `${member.user.createdAt}` },
])
.setTimestamp()
.setFooter({
text: footerText,
iconURL: footerIcon,
}),
],
})
.then(async () => {
logger.info(
`Audit log sent for event guildMemberAdd in guild ${member.guild.name} (${member.guild.id})`
);
})
.catch(async () => {
logger.error(
`Audit log failed to send for event guildMemberAdd in guild ${member.guild.name} (${member.guild.id})`
);
});
},
};

View file

@ -1,24 +0,0 @@
// 3rd party dependencies
import { GuildMember } from "discord.js";
// Dependencies
import updatePresence from "@helpers/updatePresence";
import fetchUser from "@helpers/fetchUser";
import logger from "@logger";
import joinMessage from "../guildMemberAdd/joinMessage";
import audits from "../guildMemberAdd/audits";
export default {
async execute(member: GuildMember) {
const { client, user, guild } = member;
logger?.silly(
`New member: ${user.tag} (${user.id}) added to guild: ${guild.name} (${guild.id})`
);
await audits.execute(member);
await joinMessage.execute(member);
await fetchUser(user, guild);
await updatePresence(client);
},
};

View file

@ -1,55 +0,0 @@
import logger from "@logger";
import { GuildMember, MessageEmbed, TextChannel } from "discord.js";
import guildSchema from "@schemas/guild";
import getEmbedConfig from "@helpers/getEmbedConfig";
export default {
execute: async (member: GuildMember) => {
const { footerText, footerIcon, errorColor } = await getEmbedConfig(
member.guild
);
const guildData = await guildSchema.findOne({ guildId: member.guild.id });
const { client } = member;
if (guildData === null) return;
if (guildData.audits.status !== true) return;
if (!guildData.audits.channelId) return;
const channel = client.channels.cache.get(`${guildData.audits.channelId}`);
if (channel === null) return;
(channel as TextChannel)
.send({
embeds: [
new MessageEmbed()
.setColor(errorColor)
.setAuthor({
name: "Member Left",
iconURL: member.user.displayAvatarURL(),
})
.setDescription(`${member.user} ${member.user.tag}`)
.setTimestamp()
.setFooter({
text: footerText,
iconURL: footerIcon,
}),
],
})
.then(async () => {
logger.info(
`Audit log sent for event guildMemberRemove in guild ${member.guild.name} (${member.guild.id})`
);
})
.catch(async () => {
logger.error(
`Audit log failed to send for event guildMemberRemove in guild ${member.guild.name} (${member.guild.id})`
);
});
},
};

View file

@ -1,24 +0,0 @@
// 3rd party dependencies
import { GuildMember } from "discord.js";
// Dependencies
import updatePresence from "@helpers/updatePresence";
import dropUser from "@helpers/dropUser";
import logger from "@logger";
import leaveMessage from "./leaveMessage";
import audits from "./audits";
export default {
async execute(member: GuildMember) {
const { client, user, guild } = member;
logger?.silly(
`Removed member: ${user.tag} (${user.id}) from guild: ${guild.name} (${guild.id})`
);
await audits.execute(member);
await leaveMessage.execute(member);
await dropUser(user, guild);
await updatePresence(client);
},
};

View file

@ -1,22 +0,0 @@
// 3rd party dependencies
import mongoose from "mongoose";
// Dependencies
import logger from "@logger";
// Configuration
import { url } from "@config/database";
export default async () => {
await mongoose.connect(url).then(async (connection) => {
logger.info(`Connected to database: ${connection.connection.name}`);
});
mongoose.connection.on("error", async (error) => {
logger.error(`${error}`);
});
mongoose.connection.on("warn", async (warning) => {
logger.warn(warning);
});
};

View file

@ -1,102 +0,0 @@
// Dependencies
import { CommandInteraction, MessageEmbed } from "discord.js";
import logger from "@logger";
import deferReply from "@root/helpers/deferReply";
import getEmbedConfig from "@helpers/getEmbedConfig";
import getCommandMetadata from "@helpers/getCommandMetadata";
export default async (interaction: CommandInteraction) => {
if (!interaction.isCommand()) return;
if (interaction.guild == null) return;
const { errorColor, footerText, footerIcon } = await getEmbedConfig(
interaction.guild
);
const { client, guild, commandName, user, memberPermissions } = interaction;
const currentCommand = client.commands.get(commandName);
if (currentCommand == null) {
logger.silly(`Command ${commandName} not found`);
}
const metadata = await getCommandMetadata(interaction, currentCommand);
await deferReply(interaction, metadata.ephemeral || false);
if (
metadata.permissions &&
metadata.guildOnly &&
!memberPermissions?.has(metadata.permissions)
) {
return interaction?.editReply({
embeds: [
new MessageEmbed()
.setTitle("[:x:] Permission")
.setDescription(`You do not have the permission to manage the bot.`)
.setTimestamp(new Date())
.setColor(errorColor)
.setFooter({ text: footerText, iconURL: footerIcon }),
],
});
}
if (metadata.guildOnly) {
if (!guild) {
logger.debug(`Guild is null`);
return interaction.editReply({
embeds: [
new MessageEmbed()
.setDescription("This command is only available for guild")
.setColor(errorColor)
.setTimestamp(new Date())
.setFooter({ text: footerText, iconURL: footerIcon }),
],
});
}
}
if (metadata.dmOnly) {
if (guild) {
logger.silly(`Guild exist`);
return interaction.editReply({
embeds: [
new MessageEmbed()
.setDescription("This command is only available in DM.")
.setColor(errorColor)
.setTimestamp(new Date())
.setFooter({ text: footerText, iconURL: footerIcon }),
],
});
}
}
await currentCommand
.execute(interaction)
.then(async () => {
return logger?.silly(
`Command: ${commandName} executed in guild: ${guild?.name} (${guild?.id}) by user: ${user?.tag} (${user?.id})`
);
})
.catch(async (error: any) => {
logger?.error(`${error}`);
return interaction.editReply({
embeds: [
new MessageEmbed()
.setTitle("Error")
.setDescription(
`There was an error executing the command: **${currentCommand?.data?.name}**.`
)
.setColor(errorColor)
.setTimestamp(new Date())
.setFooter({ text: footerText, iconURL: footerIcon }),
],
});
});
};

View file

@ -1,20 +0,0 @@
// 3rd party dependencies
import { CommandInteraction } from "discord.js";
// Dependencies
import isCommand from "@root/events/interactionCreate/components/isCommand";
import logger from "@logger";
import audits from "./audits";
export default {
async execute(interaction: CommandInteraction) {
const { guild, id } = interaction;
logger?.silly(
`New interaction: ${id} in guild: ${guild?.name} (${guild?.id})`
);
await audits.execute(interaction);
await isCommand(interaction);
},
};

View file

@ -1,10 +0,0 @@
import { Message } from "discord.js";
import modules from "@events/messageCreate/modules";
export default {
async execute(message: Message) {
await modules.credits.execute(message);
await modules.points.execute(message);
await modules.counters.execute(message);
},
};

View file

@ -1,85 +0,0 @@
import logger from "@logger";
import timeouts from "@schemas/timeout";
import { Message } from "discord.js";
import fetchUser from "@helpers/fetchUser";
import fetchGuild from "@helpers/fetchGuild";
export default {
execute: async (message: Message) => {
const { guild, author, content, channel } = message;
if (guild == null) return;
if (author.bot) return;
if (channel?.type !== "GUILD_TEXT") return;
const { id: guildId } = guild;
const { id: userId } = author;
const guildData = await fetchGuild(guild);
const userData = await fetchUser(author, guild);
if (content.length < guildData.credits.minimumLength) return;
const timeoutData = {
guildId,
userId,
timeoutId: "2022-04-14-13-51-00",
};
const timeout = await timeouts.findOne(timeoutData);
if (timeout) {
logger.silly(
`User ${userId} in guild ${guildId} is on timeout 2022-04-14-13-51-00`
);
return;
}
userData.credits += guildData.credits.rate;
await userData
.save()
.then(async () => {
logger.silly(
`User ${userId} in guild ${guildId} has ${userData.credits} credits`
);
})
.catch(async (err) => {
logger.error(
`Error saving credits for user ${userId} in guild ${guildId}`,
err
);
});
await timeouts
.create(timeoutData)
.then(async () => {
logger.silly(
`Timeout 2022-04-14-13-51-00 for user ${userId} in guild ${guildId} has been created`
);
})
.catch(async (err) => {
logger.error(
`Error creating timeout 2022-04-14-13-51-00 for user ${userId} in guild ${guildId}`,
err
);
});
setTimeout(async () => {
await timeouts
.deleteOne(timeoutData)
.then(async () => {
logger.silly(
`Timeout 2022-04-14-13-51-00 for user ${userId} in guild ${guildId} has been deleted`
);
})
.catch(async (err) => {
logger.error(
`Error deleting timeout 2022-04-14-13-51-00 for user ${userId} in guild ${guildId}`,
err
);
});
}, guildData.credits.timeout);
},
};

View file

@ -1,9 +0,0 @@
import counters from "@events/messageCreate/modules/counters";
import credits from "@events/messageCreate/modules/credits";
import points from "@events/messageCreate/modules/points";
export default {
counters,
credits,
points,
};

View file

@ -1,89 +0,0 @@
import logger from "@logger";
import timeouts from "@schemas/timeout";
import fetchUser from "@helpers/fetchUser";
import fetchGuild from "@helpers/fetchGuild";
import { Message } from "discord.js";
export default {
execute: async (message: Message) => {
const { guild, author, content, channel } = message;
if (guild == null) return;
if (author.bot) return;
if (channel?.type !== "GUILD_TEXT") return;
const { id: guildId } = guild;
const { id: userId } = author;
const guildData = await fetchGuild(guild);
const userData = await fetchUser(author, guild);
if (content.length < guildData.credits.minimumLength) return;
const timeoutData = {
guildId,
userId,
timeoutId: "2022-04-14-14-15-00",
};
const timeout = await timeouts.findOne(timeoutData);
if (timeout) {
logger.silly(
`User ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id} is on timeout 2022-04-14-14-15-00`
);
return;
}
userData.points += guildData.points.rate;
await userData
.save()
.then(async () => {
logger.silly(
`Successfully saved user ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`
);
})
.catch(async (err) => {
logger.error(
`Error saving points for user ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`,
err
);
});
logger.silly(
`User ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id}) has ${userData.points} points`
);
await timeouts
.create(timeoutData)
.then(async () => {
logger.silly(
`Successfully created timeout for user ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`
);
})
.catch(async (err) => {
logger.error(
`Error creating timeout 2022-04-14-14-15-00 for user ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`,
err
);
});
setTimeout(async () => {
await timeouts
.deleteOne(timeoutData)
.then(async () => {
logger.silly(
`Successfully deleted timeout 2022-04-14-14-15-00 for user ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`
);
})
.catch(async (err) => {
logger.error(
`Error deleting timeout 2022-04-14-14-15-00 for user ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`,
err
);
});
}, guildData.points.timeout);
},
};

View file

@ -1,10 +0,0 @@
import { Message } from "discord.js";
import audits from "@events/messageDelete/audits";
import counter from "./modules/counter";
export default {
async execute(message: Message) {
await audits.execute(message);
await counter(message);
},
};

View file

@ -1,24 +0,0 @@
// Dependencies
import { Message } from "discord.js";
import logger from "@logger";
// Modules
import counter from "./modules/counter";
import audits from "./audits";
export default {
async execute(oldMessage: Message, newMessage: Message) {
const { author, guild } = newMessage;
await audits.execute(oldMessage, newMessage);
logger?.silly(
`Message update event fired by ${author.tag} (${author.id}) in guild: ${guild?.name} (${guild?.id})`
);
if (author?.bot) return logger?.silly(`Message update event fired by bot`);
await counter(newMessage);
},
};

View file

@ -1,25 +0,0 @@
// Dependencies
import { Client } from "discord.js";
import logger from "@logger";
// Helpers
import updatePresence from "@helpers/updatePresence";
import deployCommands from "@handlers/deployCommands";
import devMode from "@handlers/devMode";
export default {
once: true,
async execute(client: Client) {
logger.info("Ready!");
await updatePresence(client);
await devMode(client);
await deployCommands(client);
client.guilds?.cache.forEach((guild) => {
logger.silly(
`${client.user?.tag} (${client.user?.id}) is in guild: ${guild.name} (${guild.id}) with member count of ${guild.memberCount}`
);
});
},
};

View file

@ -1,36 +0,0 @@
import fs from "fs"; // fs
import { Collection } from "discord.js"; // discord.js
import { Client } from "@root/types/common/discord";
import logger from "@logger";
export default async (client: Client) => {
client.commands = new Collection();
fs.readdir("./src/plugins", async (error, plugins) => {
if (error) {
return logger.error(`Error reading plugins: ${error}`);
}
await Promise.all(
plugins.map(async (pluginName, index) => {
const plugin = await import(`../plugins/${pluginName}`);
await client.commands.set(
plugin.default.builder.name,
plugin.default,
plugin.default.metadata
);
logger.verbose(
`Loaded plugin ${index + 1}/${plugins.length}: ${pluginName}`
);
})
)
.then(async () => {
logger.info(`Started all ${plugins.length} plugins.`);
})
.catch(async (err) => {
logger.error(`${err}`);
});
});
};

View file

@ -1,53 +0,0 @@
// Dependencies
import { token, clientId } from "@config/discord";
import { devMode, guildId } from "@config/other";
import logger from "../logger";
import { Client } from "@root/types/common/discord";
import { REST } from "@discordjs/rest";
import { Routes } from "discord-api-types/v9";
export default async (client: Client) => {
const pluginList = [] as string[];
await Promise.all(
client.commands.map(async (pluginData: any) => {
pluginList.push(pluginData.builder.toJSON());
logger.verbose(
`Plugin is ready for deployment: ${pluginData.builder.name}`
);
})
)
.then(async () => {
logger.info("All plugins are ready to be deployed.");
})
.catch(async (error) => {
logger.error(`${error}`);
});
const rest = new REST({ version: "9" }).setToken(token);
await rest
.put(Routes.applicationCommands(clientId), {
body: pluginList,
})
.then(async () => {
logger.info(`Successfully deployed plugins to Discord's API`);
})
.catch(async (error) => {
logger.error(`${error}`);
});
if (devMode) {
await rest
.put(Routes.applicationGuildCommands(clientId, guildId), {
body: pluginList,
})
.then(async () =>
logger.info(`Successfully deployed guild plugins to Discord's API`)
)
.catch(async (error) => {
logger.error(`${error}`);
});
}
};

View file

@ -0,0 +1,58 @@
import { token, clientId } from "../../config/discord";
import { devMode, guildId } from "../../config/other";
import logger from "../../logger";
import { Client } from "discord.js";
import { REST } from "@discordjs/rest";
import { Routes } from "discord-api-types/v9";
import { RESTPostAPIApplicationCommandsJSONBody } from "discord-api-types/v10";
import { ICommand } from "../../interfaces/Command";
export default async (client: Client) => {
const commandList: Array<RESTPostAPIApplicationCommandsJSONBody> = [];
if (!client.commands) {
throw new Error("client.commands is not defined");
}
logger.info("Gathering command list");
await Promise.all(
client.commands.map(async (commandData: ICommand) => {
commandList.push(commandData.builder.toJSON());
logger.verbose(`${commandData.builder.name} pushed to list`);
})
)
.then(async () => {
logger.info(`Finished gathering command list.`);
})
.catch(async (error) => {
throw new Error(`Could not gather command list: ${error}`);
});
const rest = new REST({ version: "9" }).setToken(token);
await rest
.put(Routes.applicationCommands(clientId), {
body: commandList,
})
.then(async () => {
logger.info(`Finished updating command list.`);
})
.catch(async (error) => {
logger.error(`${error}`);
});
if (devMode) {
await rest
.put(Routes.applicationGuildCommands(clientId, guildId), {
body: commandList,
})
.then(async () => logger.info(`Finished updating guild command list.`))
.catch(async (error) => {
logger.error(`${error}`);
});
}
};

View file

@ -1,10 +1,10 @@
// Dependencies
import { Client } from "discord.js";
import logger from "@logger";
import logger from "../../logger";
// Configuration
import { devMode, guildId } from "@config/other";
import { devMode, guildId } from "../../config/other";
export default async (client: Client) => {
if (!devMode) {

View file

@ -1,12 +1,13 @@
import crypto from "crypto";
import { secretKey, algorithm } from "@config/encryption";
import { secretKey, algorithm } from "../../config/encryption";
import { IEncryptionData } from "../../interfaces/EncryptionData";
const iv = crypto.randomBytes(16);
const encrypt = (text: any): { iv: any; content: any } => {
const encrypt = (text: crypto.BinaryLike): IEncryptionData => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return {
@ -15,7 +16,7 @@ const encrypt = (text: any): { iv: any; content: any } => {
};
};
const decrypt = (hash: any) => {
const decrypt = (hash: IEncryptionData) => {
const decipher = crypto.createDecipheriv(
algorithm,
secretKey,

View file

@ -1,37 +0,0 @@
import fs from "fs"; // fs
import { Client } from "discord.js"; // discord.js
import logger from "@logger";
export default async (client: Client) => {
fs.readdir("./src/events", async (error, events) => {
if (error) {
return logger.error(`Error reading plugins: ${error}`);
}
await Promise.all(
events.map(async (eventName, index) => {
const event = await import(`../events/${eventName}`);
logger.verbose(
`Loaded event ${index + 1}/${events.length}: ${eventName}`
);
if (event.once) {
return client.once(eventName, async (...args) =>
event.default.execute(...args)
);
}
return client.on(eventName, async (...args) =>
event.default.execute(...args)
);
})
)
.then(async () => {
logger.info(`Started all ${events.length} events.`);
})
.catch(async (err) => {
logger.error(`${err}`);
});
});
};

View file

@ -1,24 +0,0 @@
// Dependencies
import { Client } from "discord.js";
import schedule from "node-schedule";
import logger from "@logger";
// Jobs
import shopRoles from "@jobs/shopRoles";
export default async (client: Client) => {
const expression = "*/5 * * * *";
schedule.scheduleJob(expression, async () => {
logger.info("Running jobs.");
await shopRoles(client)
.then(() => {
logger.info("Shop roles job finished.");
})
.catch((err) => {
logger.error(`Shop roles job failed: ${err}`);
});
});
};

View file

@ -0,0 +1,4 @@
export default async (seconds: number, date: Date) => {
date.setSeconds(date.getSeconds() + seconds);
return date;
};

View file

@ -0,0 +1,3 @@
export default (text: string): string => {
return text.charAt(0).toUpperCase() + text.slice(1);
};

View file

@ -0,0 +1,156 @@
// Dependencies
import { CommandInteraction, ButtonInteraction, Message } from "discord.js";
import logger from "../../logger";
import timeoutSchema from "../../models/timeout";
import addSeconds from "../../helpers/addSeconds";
export const command = async (i: CommandInteraction, cooldown: number) => {
const { guild, user, commandId } = i;
// Check if user has a timeout
const hasTimeout = await timeoutSchema.findOne({
guildId: guild?.id || "0",
userId: user.id,
cooldown: cooldown,
timeoutId: commandId,
});
// If user is not on timeout
if (hasTimeout) {
const { guildId, userId, timeoutId, createdAt } = hasTimeout;
const overDue = (await addSeconds(cooldown, createdAt)) < new Date();
if (!overDue) {
const diff = Math.round(
(new Date(hasTimeout.createdAt).getTime() - new Date().getTime()) / 1000
);
throw new Error(
`You must wait ${diff} seconds before using this command.`
);
}
// Delete timeout
await timeoutSchema
.deleteOne({
guildId,
userId,
timeoutId,
cooldown,
})
.then(async () => {
logger.debug(
`Timeout document ${timeoutId} has been deleted from user ${userId}.`
);
});
}
// Create timeout
await timeoutSchema.create({
guildId: guild?.id || "0",
userId: user.id,
cooldown: cooldown,
timeoutId: commandId,
});
};
export const button = async (i: ButtonInteraction, cooldown: number) => {
const { guild, user, customId } = i;
// Check if user has a timeout
const hasTimeout = await timeoutSchema.findOne({
guildId: guild?.id || "0",
userId: user.id,
cooldown: cooldown,
timeoutId: customId,
});
// If user is not on timeout
if (hasTimeout) {
const { guildId, userId, timeoutId, createdAt } = hasTimeout;
const overDue = (await addSeconds(cooldown, createdAt)) < new Date();
if (!overDue) {
const diff = Math.round(
(new Date(hasTimeout.createdAt).getTime() - new Date().getTime()) / 1000
);
throw new Error(
`You must wait ${diff} seconds before using this command.`
);
}
// Delete timeout
await timeoutSchema
.deleteOne({
guildId,
userId,
timeoutId,
cooldown,
})
.then(async () => {
logger.debug(
`Timeout document ${timeoutId} has been deleted from user ${userId}.`
);
});
}
// Create timeout
await timeoutSchema.create({
guildId: guild?.id || "0",
userId: user.id,
cooldown: cooldown,
timeoutId: customId,
});
};
export const message = async (msg: Message, cooldown: number, id: string) => {
const { guild, member } = msg;
if (!guild) throw new Error("Guild is undefined");
if (!member) throw new Error("Member is undefined");
// Check if user has a timeout
const hasTimeout = await timeoutSchema.findOne({
guildId: guild?.id || "0",
userId: member.id,
cooldown: cooldown,
timeoutId: id,
});
// If user is not on timeout
if (hasTimeout) {
const { guildId, userId, timeoutId, createdAt } = hasTimeout;
const overDue = (await addSeconds(cooldown, createdAt)) < new Date();
if (!overDue) {
const diff = Math.round(
(new Date(hasTimeout.createdAt).getTime() - new Date().getTime()) / 1000
);
throw new Error(
`User: ${userId} on timeout-id: ${id} with cooldown: ${cooldown} secs with remaining: ${diff} secs.`
);
}
// Delete timeout
await timeoutSchema
.deleteOne({
guildId,
userId: member.id,
timeoutId: id,
cooldown,
})
.then(async () => {
logger.debug(
`Timeout document ${timeoutId} has been deleted from user ${userId}.`
);
});
}
// Create timeout
await timeoutSchema.create({
guildId: guild?.id || "0",
userId: member.id,
cooldown: cooldown,
timeoutId: id,
});
};

View file

@ -1,28 +0,0 @@
import { CommandInteraction, MessageEmbed } from "discord.js";
import getEmbedConfig from "@helpers/getEmbedConfig";
export default async (interaction: CommandInteraction, ephemeral: boolean) => {
if (interaction.guild == null) return;
await interaction.deferReply({
ephemeral,
});
const { waitColor, footerText, footerIcon } = await getEmbedConfig(
interaction.guild
);
await interaction.editReply({
embeds: [
new MessageEmbed()
.setFooter({
text: footerText,
iconURL: footerIcon,
})
.setTimestamp(new Date())
.setTitle("Processing your request")
.setColor(waitColor)
.setDescription("Please wait..."),
],
});
};

View file

@ -0,0 +1,27 @@
import { Interaction, MessageEmbed } from "discord.js";
import getEmbedConfig from "../../helpers/getEmbedConfig";
export default async (interaction: Interaction, ephemeral: boolean) => {
if (!interaction.isRepliable())
throw new Error(`Cannot reply to an interaction that is not repliable`);
await interaction.deferReply({
ephemeral,
});
const embedConfig = await getEmbedConfig(interaction.guild);
await interaction.editReply({
embeds: [
new MessageEmbed()
.setFooter({
text: embedConfig.footerText,
iconURL: embedConfig.footerIcon,
})
.setTimestamp(new Date())
.setTitle("Processing your request")
.setColor(embedConfig.waitColor)
.setDescription("Please wait..."),
],
});
};

View file

@ -1,11 +1,11 @@
import guildSchema from "@schemas/guild";
import userSchema from "@schemas/user";
import apiSchema from "@schemas/api";
import counterSchema from "@schemas/counter";
import shopRoleSchema from "@schemas/shopRole";
import timeoutSchema from "@schemas/timeout";
import guildSchema from "../../models/guild";
import userSchema from "../../models/user";
import apiSchema from "../../models/api";
import counterSchema from "../../models/counter";
import shopRoleSchema from "../../models/shopRole";
import timeoutSchema from "../../models/timeout";
import logger from "@logger";
import logger from "../../logger";
import { Guild } from "discord.js";

View file

@ -1,6 +1,6 @@
import userSchema from "@schemas/user";
import userSchema from "../../models/user";
import logger from "@logger";
import logger from "../../logger";
import { Guild, User } from "discord.js";

View file

@ -1,4 +1,4 @@
import { footerText, footerIcon } from "@config/embed";
import { footerText, footerIcon } from "../../config/embed";
import { MessageEmbed } from "discord.js";
export default new MessageEmbed()

View file

@ -2,10 +2,10 @@
import { Guild } from "discord.js";
// Models
import guildSchema from "@schemas/guild";
import guildSchema from "../../models/guild";
// Handlers
import logger from "@logger";
import logger from "../../logger";
// Function
export default async (guild: Guild) => {

View file

@ -2,10 +2,10 @@
import { Guild, User } from "discord.js";
// Models
import userSchema from "@schemas/user";
import userSchema from "../../models/user";
// Handlers
import logger from "@logger";
import logger from "../../logger";
// Function
export default async (user: User, guild: Guild) => {

View file

@ -1,12 +0,0 @@
import { CommandInteraction } from "discord.js";
export default async (interaction: CommandInteraction, currentCommand: any) => {
const subcommand = interaction.options.getSubcommand();
const subcommandGroup = interaction.options.getSubcommandGroup(false);
if (!subcommandGroup) {
return currentCommand.modules[subcommand].metadata;
}
return currentCommand.modules[subcommandGroup].modules[subcommand].metadata;
};

View file

@ -0,0 +1,14 @@
import { CommandInteraction } from "discord.js";
import { ICommand } from "../../interfaces/Command";
export default async (
interaction: CommandInteraction,
currentCommand: ICommand
) => {
const subcommand = interaction.options.getSubcommand();
const subcommandGroup = interaction.options.getSubcommandGroup(false);
return subcommandGroup
? currentCommand.moduleData[subcommandGroup].moduleData[subcommand].metadata
: currentCommand.moduleData[subcommand].metadata;
};

View file

@ -1,18 +0,0 @@
import guildSchema from "@schemas/guild";
import { ColorResolvable, Guild } from "discord.js";
export default async (guild: Guild) => {
const guildConfig = await guildSchema.findOne({ guildId: guild.id });
if (guildConfig == null)
return {
successColor: "#22bb33" as ColorResolvable,
waitColor: "#f0ad4e" as ColorResolvable,
errorColor: "#bb2124" as ColorResolvable,
footerIcon: "https://github.com/ZynerOrg.png",
footerText: "https://github.com/ZynerOrg/xyter",
};
return guildConfig.embeds;
};

View file

@ -0,0 +1,18 @@
import guildSchema from "../../models/guild";
import * as embedConfig from "../../config/embed";
import { Guild } from "discord.js";
export default async (guild?: Guild | null) => {
if (!guild) {
return { ...embedConfig };
}
const guildConfig = await guildSchema.findOne({ guildId: guild.id });
if (!guildConfig) {
return {
...embedConfig,
};
}
return guildConfig.embeds;
};

View file

@ -0,0 +1,8 @@
import fs from "fs";
const fsPromises = fs.promises;
export default async (path: string) => {
return fsPromises.readdir(path).catch(async (err) => {
throw new Error(`Could not list directory: ${path}`, err);
});
};

View file

@ -1,4 +1,4 @@
import logger from "@root/logger";
import logger from "../../logger";
export default (count: number, noun: string, suffix?: string): string => {
const result = `${count} ${noun}${count !== 1 ? suffix || "s" : ""}`;

View file

@ -1,43 +0,0 @@
import sleep from "@helpers/sleep";
import logger from "@logger";
import Chance from "chance";
export default async function saveUser(data: any, data2: any) {
process.nextTick(
async () => {
// Chance module
const chance = new Chance();
await sleep(
chance.integer({
min: 0,
max: 1,
}) *
10 +
1 * 100
); // 100 - 1000 random Number generator
data.save((_: any) =>
_
? logger?.error(
`ERROR Occurred while saving data (saveUser) \n${"=".repeat(
50
)}\n${`${_}\n${"=".repeat(50)}`}`
)
: logger?.silly(`Saved user: ${data.id} (saveUser)`)
);
if (data2) {
data2.save((_: any) =>
_
? logger?.error(
`ERROR Occurred while saving data (saveUser) \n${"=".repeat(
50
)}\n${`${_}\n${"=".repeat(50)}`}`
)
: logger?.silly(`Saved user: ${data2.id} (saveUser)`)
);
}
},
data,
data2
);
}

View file

@ -1,8 +0,0 @@
import logger from "@logger";
export default function sleep(milliseconds: any) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
logger?.silly(`Sleeping for ${milliseconds} milliseconds`);
});
}

View file

@ -1,14 +0,0 @@
// Dependencies
import { Client } from "discord.js";
import logger from "@logger";
// Function
export default async (client: Client) => {
const status = `${client?.guilds?.cache?.size} guilds.`;
client?.user?.setPresence({
activities: [{ type: "WATCHING", name: status }],
status: "online",
});
logger?.debug(`Updated client presence to: ${status}`);
};

View file

@ -0,0 +1,20 @@
// Dependencies
import { Client } from "discord.js";
import logger from "../../logger";
// Function
export default async (client: Client) => {
if (!client?.user) throw new Error("Client's user is undefined.");
const { guilds } = client;
const memberCount = guilds.cache.reduce((a, g) => a + g.memberCount, 0);
const guildCount = guilds.cache.size;
const status = `${memberCount} users in ${guildCount} guilds.`;
client.user.setPresence({
activities: [{ type: "LISTENING", name: status }],
status: "online",
});
logger.info(`Client's presence is set to "${status}"`);
};

View file

@ -1,13 +1,10 @@
import "tsconfig-paths/register"; // Allows using tsconfig.json paths during runtime
import { token, intents } from "@config/discord";
import { token, intents } from "./config/discord";
import { Client } from "discord.js"; // discord.js
import database from "@database";
import schedules from "@handlers/schedules";
import events from "@handlers/events";
import commands from "@handlers/commands";
import * as managers from "./managers";
// Main process that starts all other sub processes
const main = async () => {
@ -16,17 +13,7 @@ const main = async () => {
intents,
});
// Start database manager
await database();
// Start schedule manager
await schedules(client);
// Start command handler
await commands(client);
// Start event handler
await events(client);
await managers.start(client);
// Authorize with Discord's API
await client.login(token);

View file

@ -0,0 +1,7 @@
import { SlashCommandBuilder } from "@discordjs/builders";
export interface ICommand {
builder: SlashCommandBuilder;
moduleData: any;
execute: Promise<void>;
}

View file

@ -0,0 +1,4 @@
export interface IEncryptionData {
iv: string;
content: string;
}

6
src/interfaces/Event.ts Normal file
View file

@ -0,0 +1,6 @@
import { IEventOptions } from "./EventOptions";
export interface IEvent {
options: IEventOptions;
execute: (...args: Promise<void>[]) => Promise<void>;
}

View file

@ -0,0 +1,3 @@
export interface IEventOptions {
type: "on" | "once";
}

8
src/interfaces/Job.ts Normal file
View file

@ -0,0 +1,8 @@
import { Client } from "discord.js";
export interface IJob {
options: {
schedule: string;
};
execute: (client: Client) => Promise<void>;
}

View file

@ -0,0 +1,9 @@
import { Snowflake } from "discord.js";
import { Document } from "mongoose";
export interface IShopRole extends Document {
guildId: Snowflake;
userId: Snowflake;
roleId: Snowflake;
lastPayed: Date;
}

12
src/jobs/shop/index.ts Normal file
View file

@ -0,0 +1,12 @@
// Dependencies
import { Client } from "discord.js";
import * as roles from "./modules/roles";
export const options = {
schedule: "*/5 * * * *", // https://crontab.guru/
};
export const execute = async (client: Client) => {
await roles.execute(client);
};

View file

@ -0,0 +1,10 @@
import { Client } from "discord.js";
import logger from "../../../../../logger";
import { IShopRole } from "../../../../../interfaces/ShopRole";
export const execute = async (_client: Client, role: IShopRole) => {
const { roleId } = role;
logger.silly(`Shop role ${roleId} is not due for payment.`);
};

View file

@ -0,0 +1,85 @@
import { Client } from "discord.js";
import logger from "../../../../../logger";
import { IShopRole } from "../../../../../interfaces/ShopRole";
import guildSchema from "../../../../../models/guild";
import userSchema from "../../../../../models/user";
import shopRoleSchema from "../../../../../models/shopRole";
export const execute = async (client: Client, role: IShopRole) => {
const { guildId, userId, roleId } = role;
if (!userId) throw new Error("User ID not found for shop role.");
const guildData = await guildSchema.findOne({ guildId });
if (!guildData) throw new Error("Guild not found.");
const userData = await userSchema.findOne({ guildId, userId });
if (!userData) throw new Error("User not found.");
const rGuild = client.guilds.cache.get(guildId);
if (!rGuild) throw new Error("Guild not found.");
const rMember = await rGuild.members.fetch(userId);
if (!rMember) throw new Error("Member not found.");
const rRole = rMember.roles.cache.get(roleId);
if (!rRole) throw new Error("Role not found.");
logger.debug(`Shop role ${roleId} is due for payment.`);
const { pricePerHour } = guildData.shop.roles;
if (userData.credits < pricePerHour) {
await rMember.roles
.remove(roleId)
.then(async () => {
await shopRoleSchema
.deleteOne({
userId,
roleId,
guildId,
})
.then(async () => {
logger.silly(
`Shop role document ${roleId} has been deleted from user ${userId}.`
);
})
.catch(async (err) => {
throw new Error(
`Error deleting shop role document ${roleId} from user ${userId}.`,
err
);
});
})
.catch(async (err) => {
throw new Error(
`Error removing role ${roleId} from user ${userId}.`,
err
);
});
throw new Error("User does not have enough credits.");
}
userData.credits -= pricePerHour;
await userData
.save()
.then(async () => {
logger.silly(`User ${userId} has been updated.`);
role.lastPayed = new Date();
await role
.save()
.then(async () => {
logger.silly(`Shop role ${roleId} has been updated.`);
})
.catch(async (err) => {
throw new Error(`Error updating shop role ${roleId}.`, err);
});
logger.debug(`Shop role ${roleId} has been paid.`);
})
.catch(async (err) => {
throw new Error(`Error updating user ${userId}.`, err);
});
};

View file

@ -0,0 +1,32 @@
import { Client } from "discord.js";
import { IShopRole } from "../../../../interfaces/ShopRole";
import shopRoleSchema from "../../../../models/shopRole";
import * as overDueForPayment from "./components/overDueForPayment";
import * as dueForPayment from "./components/dueForPayment";
export const execute = async (client: Client) => {
const roles = await shopRoleSchema.find();
await Promise.all(
roles.map(async (role: IShopRole) => {
const { lastPayed } = role;
const nextPayment = new Date(
lastPayed.setHours(lastPayed.getHours() + 1)
);
const now = new Date();
if (nextPayment > now) {
await dueForPayment.execute(client, role);
return;
}
if (nextPayment < now) {
await overDueForPayment.execute(client, role);
}
})
);
};

View file

@ -1,134 +0,0 @@
// Dependencies
import { Client } from "discord.js";
import logger from "@logger";
// Schemas
import userSchema from "@schemas/user";
import shopRoleSchema from "@schemas/shopRole";
import guildSchema from "@schemas/guild";
export default async (client: Client) => {
const roles = await shopRoleSchema.find();
await Promise.all(
roles.map(async (role) => {
const { guildId, userId, roleId } = role;
const lastPayment = new Date(role.lastPayed);
const nextPayment = new Date(
lastPayment.setHours(lastPayment.getHours() + 1)
);
if (new Date() < nextPayment) {
logger.silly(`Shop role ${roleId} is not due for payment.`);
}
const guildData = await guildSchema.findOne({ guildId });
if (!guildData) {
logger.error(`Guild ${guildId} not found.`);
return;
}
if (!userId) {
logger.error(`User ID not found for shop role ${roleId}.`);
return;
}
const userData = await userSchema.findOne({ guildId, userId });
if (!userData) {
logger.error(`User ${userId} not found for shop role ${roleId}.`);
return;
}
const rGuild = client?.guilds?.cache?.get(guildId);
const rMember = await rGuild?.members?.fetch(userId);
if (!rMember) {
logger.error(`Member ${userId} not found for shop role ${roleId}.`);
return;
}
const rRole = rMember.roles.cache.get(roleId);
if (!rMember || !rRole) {
logger.error(`Member ${userId} not found for shop role ${roleId}.`);
await shopRoleSchema
.deleteOne({
userId,
roleId,
guildId,
})
.then(async () => {
logger.silly(
`Shop role document ${roleId} has been deleted from user ${userId}.`
);
})
.catch(async (error) => {
logger.error(
`Error deleting shop role document ${roleId} from user ${userId}.`,
error
);
});
return;
}
if (new Date() > nextPayment) {
logger.silly(
`Shop role ${roleId} is due for payment. Withdrawing credits from user ${userId}.`
);
const { pricePerHour } = guildData.shop.roles;
if (userData.credits < pricePerHour) {
logger.error(
`User ${userId} does not have enough credits to pay for shop role ${roleId}.`
);
if (!rMember) {
logger.error(`Member ${userId} not found for shop role ${roleId}.`);
return;
}
rMember.roles.remove(roleId);
return;
}
userData.credits -= pricePerHour;
await userData
.save()
.then(async () => {
role.lastPayed = new Date();
await role
.save()
.then(async () => {
logger.silly(`Shop role ${roleId} has been paid for.`);
})
.catch(async (err) => {
logger.error(
`Error saving shop role ${roleId} last payed date.`,
err
);
});
logger.silly(
`Shop role ${roleId} has been paid for. Keeping role ${roleId} for user ${userId}.`
);
})
.catch(async (err) => {
logger.error(
`Error saving user ${userId} credits for shop role ${roleId}.`,
err
);
});
}
})
);
};

View file

@ -0,0 +1,35 @@
import logger from "../../logger";
import timeoutSchema from "../../models/timeout";
import addSeconds from "../../helpers/addSeconds";
export const options = {
schedule: "*/30 * * * *", // https://crontab.guru/
};
export const execute = async () => {
const timeouts = await timeoutSchema.find();
await Promise.all(
timeouts.map(async (timeout) => {
const { guildId, userId, timeoutId, cooldown, createdAt } = timeout;
const overDue = (await addSeconds(cooldown, createdAt)) < new Date();
if (overDue) {
timeoutSchema
.deleteOne({
guildId,
userId,
timeoutId,
cooldown,
})
.then(async () => {
logger.debug(
`Timeout document ${timeoutId} has been deleted from user ${userId}.`
);
});
}
})
);
};

View file

@ -1,10 +1,13 @@
import winston from "winston";
import "winston-daily-rotate-file";
const { combine, timestamp, printf, colorize, align, json } = winston.format;
import { logLevel } from "../config/other";
const { combine, timestamp, printf, errors, colorize, align, json } =
winston.format;
export default winston.createLogger({
level: process.env.LOG_LEVEL || "silly",
level: logLevel || "info",
transports: [
new winston.transports.DailyRotateFile({
filename: "logs/combined-%DATE%.log",
@ -14,6 +17,7 @@ export default winston.createLogger({
}),
new winston.transports.Console({
format: combine(
errors({ stack: true, trace: true }), // <-- use errors format
colorize({ all: true }),
timestamp({
format: "YYYY-MM-DD HH:MM:ss",

View file

@ -0,0 +1,35 @@
import { Collection, Client } from "discord.js";
import listDir from "../../helpers/listDir";
import logger from "../../logger";
import { ICommand } from "../../interfaces/Command";
export const register = async (client: Client) => {
client.commands = new Collection();
const commandNames = await listDir("plugins/commands");
if (!commandNames) throw new Error("Could not list commands");
logger.info(`Loading ${commandNames.length} commands`);
await Promise.all(
commandNames.map(async (commandName) => {
const command: ICommand = await import(
`../../plugins/commands/${commandName}`
).catch(async (e) => {
throw new Error(`Could not load command: ${commandName}`, e);
});
client.commands.set(command.builder.name, command);
logger.verbose(`${command.builder.name} loaded`);
})
)
.then(async () => {
logger.info(`Finished loading commands.`);
})
.catch(async (err) => {
throw new Error(`Could not load commands: ${err}`);
});
};

View file

@ -0,0 +1,27 @@
// 3rd party dependencies
import mongoose from "mongoose";
// Dependencies
import logger from "../../logger";
// Configuration
import { url } from "../../config/database";
export const start = async () => {
await mongoose
.connect(url)
.then(async (connection) => {
logger.info(`Connected to database: ${connection.connection.name}`);
})
.catch(async (e) => {
logger.error("Could not connect to database", e);
});
mongoose.connection.on("error", async (error) => {
logger.error(`${error}`);
});
mongoose.connection.on("warn", async (warning) => {
logger.warn(warning);
});
};

View file

@ -0,0 +1,29 @@
/* eslint-disable no-loops/no-loops */
import { Client } from "discord.js";
import listDir from "../../helpers/listDir";
import { IEvent } from "../../interfaces/Event";
import logger from "../../logger";
export const register = async (client: Client) => {
const eventNames = await listDir("plugins/events");
if (!eventNames) return;
for await (const eventName of eventNames) {
const event: IEvent = await import(`../../plugins/events/${eventName}`);
const eventExecutor = async (...args: Promise<void>[]) =>
event.execute(...args).catch(async (err) => {
logger.error(`${err}`);
});
if (!event.options?.type) return;
switch (event.options.type) {
case "once":
client.once(eventName, eventExecutor);
break;
case "on":
client.on(eventName, eventExecutor);
break;
}
}
};

13
src/managers/index.ts Normal file
View file

@ -0,0 +1,13 @@
import { Client } from "discord.js";
import * as database from "./database";
import * as schedule from "./schedule";
import * as event from "./event";
import * as command from "./command";
export const start = async (client: Client) => {
await database.start();
await schedule.start(client);
await command.register(client);
await event.register(client);
};

View file

@ -0,0 +1,29 @@
import logger from "../../logger";
import { Client } from "discord.js";
import { IJob } from "../../interfaces/Job";
import listDir from "../../helpers/listDir";
import schedule from "node-schedule";
export const start = async (client: Client) => {
logger.info("Starting schedule manager...");
const jobNames = await listDir("jobs");
if (!jobNames) return logger.info("No jobs found");
await Promise.all(
jobNames.map(async (jobName) => {
const job: IJob = await import(`../../jobs/${jobName}`);
schedule.scheduleJob(job.options.schedule, async () => {
logger.info(`Executed job ${jobName}!`);
await job.execute(client);
});
})
).then(async () => {
const list = schedule.scheduledJobs;
logger.silly(list);
});
};

View file

@ -1,10 +1,11 @@
import { Snowflake } from "discord.js";
import { model, Schema } from "mongoose";
import { IEncryptionData } from "../interfaces/EncryptionData";
export interface IApi {
guildId: Snowflake;
url: string;
token: { iv: string; content: string };
url: IEncryptionData;
token: IEncryptionData;
}
const apiSchema = new Schema<IApi>(
@ -16,11 +17,18 @@ const apiSchema = new Schema<IApi>(
index: true,
},
url: {
type: String,
required: true,
unique: false,
index: true,
default: "https://localhost/api/",
iv: {
type: String,
required: true,
unique: false,
index: true,
},
content: {
type: String,
required: true,
unique: false,
index: true,
},
},
token: {
iv: {
@ -28,14 +36,12 @@ const apiSchema = new Schema<IApi>(
required: true,
unique: false,
index: true,
default: "token",
},
content: {
type: String,
required: true,
unique: false,
index: true,
default: "token",
},
},
},

View file

@ -4,7 +4,10 @@ import { Schema, model } from "mongoose";
export interface ITimeout {
userId: Snowflake;
guildId: Snowflake;
cooldown: number;
timeoutId: string;
createdAt: Date;
updatedAt: Date;
}
const timeoutSchema = new Schema<ITimeout>(
@ -21,6 +24,12 @@ const timeoutSchema = new Schema<ITimeout>(
unique: false,
index: true,
},
cooldown: {
type: Number,
required: true,
unique: false,
index: true,
},
timeoutId: { type: String },
},
{ timestamps: true }

View file

@ -0,0 +1,8 @@
import { ButtonInteraction } from "discord.js";
import logger from "../../../logger";
export const metadata = { guildOnly: false, ephemeral: false };
export const execute = async (interaction: ButtonInteraction) => {
logger.debug(interaction.customId, "primary button clicked!");
};

View file

@ -0,0 +1,18 @@
import { CommandInteraction } from "discord.js";
import { SlashCommandBuilder } from "@discordjs/builders";
import modules from "../../commands/counters/modules";
export const builder = new SlashCommandBuilder()
.setName("counters")
.setDescription("View guild counters")
.addSubcommand(modules.view.builder);
export const moduleData = modules;
export const execute = async (interaction: CommandInteraction) => {
if (interaction.options.getSubcommand() === "view") {
await modules.view.execute(interaction);
}
};

View file

@ -0,0 +1,3 @@
import view from "./view";
export default { view };

View file

@ -1,10 +1,10 @@
import getEmbedConfig from "@helpers/getEmbedConfig";
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
import { CommandInteraction, MessageEmbed } from "discord.js";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import { ChannelType } from "discord-api-types/v10";
import counterSchema from "@schemas/counter";
import counterSchema from "../../../../../models/counter";
export default {
metadata: { guildOnly: true, ephemeral: false },
@ -25,7 +25,6 @@ export default {
},
execute: async (interaction: CommandInteraction) => {
if (interaction.guild == null) return;
const { errorColor, successColor, footerText, footerIcon } =
await getEmbedConfig(interaction.guild);
const { options, guild } = interaction;

View file

@ -0,0 +1,37 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { CommandInteraction } from "discord.js";
import logger from "../../../logger";
import modules from "./modules";
export const builder = new SlashCommandBuilder()
.setName("credits")
.setDescription("Manage your credits.")
.addSubcommand(modules.balance.builder)
.addSubcommand(modules.gift.builder)
.addSubcommand(modules.top.builder)
.addSubcommand(modules.work.builder);
export const moduleData = modules;
export const execute = async (interaction: CommandInteraction) => {
const { options } = interaction;
switch (options.getSubcommand()) {
case "balance":
await modules.balance.execute(interaction);
break;
case "gift":
await modules.gift.execute(interaction);
break;
case "top":
await modules.top.execute(interaction);
break;
case "work":
await modules.work.execute(interaction);
break;
default:
logger.silly(`Unknown subcommand ${options.getSubcommand()}`);
}
};

View file

@ -1,10 +1,10 @@
import getEmbedConfig from "@helpers/getEmbedConfig";
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
import { CommandInteraction, MessageEmbed } from "discord.js";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import logger from "@logger";
import logger from "../../../../../logger";
import fetchUser from "@helpers/fetchUser";
import fetchUser from "../../../../../helpers/fetchUser";
export default {
metadata: { guildOnly: true, ephemeral: true },
@ -19,7 +19,6 @@ export default {
);
},
execute: async (interaction: CommandInteraction) => {
if (interaction.guild == null) return;
const { errorColor, successColor, footerText, footerIcon } =
await getEmbedConfig(interaction.guild);
const { options, user, guild } = interaction;

View file

@ -2,16 +2,15 @@
import { CommandInteraction, MessageEmbed } from "discord.js";
// Configurations
import getEmbedConfig from "@helpers/getEmbedConfig";
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
// Handlers
import logger from "@logger";
import logger from "../../../../../logger";
// Helpers
import saveUser from "@helpers/saveUser";
import mongoose from "mongoose";
// Models
import fetchUser from "@helpers/fetchUser";
import fetchUser from "../../../../../helpers/fetchUser";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
// Function
@ -39,7 +38,6 @@ export default {
);
},
execute: async (interaction: CommandInteraction) => {
if (interaction.guild == null) return;
const { errorColor, successColor, footerText, footerIcon } =
await getEmbedConfig(interaction.guild);
const { options, user, guild, client } = interaction;
@ -184,53 +182,69 @@ export default {
});
}
// Withdraw amount from fromUserDB
fromUserDB.credits -= optionAmount;
const session = await mongoose.startSession();
// Deposit amount to toUserDB
toUserDB.credits += optionAmount;
session.startTransaction();
// Save users
await saveUser(fromUserDB, toUserDB).then(async () => {
// Get DM user object
const dmUser = client.users.cache.get(optionUser.id);
try {
// Withdraw amount from fromUserDB
fromUserDB.credits -= optionAmount;
if (dmUser == null) return;
// Deposit amount to toUserDB
toUserDB.credits += optionAmount;
// Send DM to user
await dmUser
.send({
embeds: [
embed
.setDescription(
`${
user.tag
} has gifted you ${optionAmount} credits with reason: ${
optionReason || "unspecified"
}`
)
.setColor(successColor),
],
})
.catch(async (error) =>
logger.error(`[Gift] Error sending DM to user: ${error}`)
);
await fromUserDB.save();
logger.silly(
`[Gift] Successfully gifted ${optionAmount} credits to ${optionUser.tag}`
);
await toUserDB.save();
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
session.endSession();
logger.error(`${error}`);
return interaction.editReply({
embeds: [
embed
.setDescription(
`Successfully gifted ${optionAmount} credits to ${
optionUser.tag
} with reason: ${optionReason || "unspecified"}`
"An error occurred while trying to gift credits. Please try again."
)
.setColor(successColor),
.setColor(errorColor),
],
});
} finally {
// ending the session
session.endSession();
}
// Get DM user object
const dmUser = client.users.cache.get(optionUser.id);
if (!dmUser) throw new Error("User not found");
// Send DM to user
await dmUser.send({
embeds: [
embed
.setDescription(
`${user.tag} has gifted you ${optionAmount} credits with reason: ${
optionReason || "unspecified"
}`
)
.setColor(successColor),
],
});
return interaction.editReply({
embeds: [
embed
.setDescription(
`Successfully gifted ${optionAmount} credits to ${
optionUser.tag
} with reason: ${optionReason || "unspecified"}`
)
.setColor(successColor),
],
});
},
};

View file

@ -0,0 +1,6 @@
import balance from "./balance";
import gift from "./gift";
import top from "./top";
import work from "./work";
export default { balance, gift, top, work };

View file

@ -1,10 +1,10 @@
import getEmbedConfig from "@helpers/getEmbedConfig";
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
import { CommandInteraction, MessageEmbed } from "discord.js";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import logger from "@logger";
import logger from "../../../../../logger";
import userSchema, { IUser } from "@schemas/user";
import userSchema, { IUser } from "../../../../../models/user";
export default {
metadata: { guildOnly: true, ephemeral: false },
@ -13,7 +13,6 @@ export default {
return command.setName("top").setDescription(`View the top users`);
},
execute: async (interaction: CommandInteraction) => {
if (interaction.guild == null) return;
const { errorColor, successColor, footerText, footerIcon } =
await getEmbedConfig(interaction.guild);
const { guild } = interaction;
@ -55,7 +54,7 @@ export default {
embeds: [
embed
.setDescription(
`Below are the top 10 users in this guild.
`Below are the top ten members in this guild.
${topTen.map(entry).join("\n")}
`

View file

@ -4,17 +4,17 @@ import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import Chance from "chance";
// Configurations
import getEmbedConfig from "@helpers/getEmbedConfig";
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
// Handlers
import logger from "@logger";
import logger from "../../../../../logger";
// Models
import timeoutSchema from "@schemas/timeout";
import * as cooldown from "../../../../../helpers/cooldown";
// Helpers
import fetchUser from "@helpers/fetchUser";
import fetchGuild from "@helpers/fetchGuild";
import fetchUser from "../../../../../helpers/fetchUser";
import fetchGuild from "../../../../../helpers/fetchGuild";
export default {
metadata: { guildOnly: true, ephemeral: true },
@ -23,9 +23,9 @@ export default {
return command.setName("work").setDescription(`Work to earn credits`);
},
execute: async (interaction: CommandInteraction) => {
if (interaction.guild == null) return;
const { errorColor, successColor, footerText, footerIcon } =
await getEmbedConfig(interaction.guild); // Destructure member
const { successColor, footerText, footerIcon } = await getEmbedConfig(
interaction.guild
); // Destructure member
const { guild, user } = interaction;
const embed = new MessageEmbed()
@ -39,33 +39,13 @@ export default {
// Chance module
const chance = new Chance();
// Check if user has a timeout
const isTimeout = await timeoutSchema?.findOne({
guildId: guild?.id,
userId: user?.id,
timeoutId: "2022-03-15-19-16",
});
if (guild === null) {
return logger?.silly(`Guild is null`);
}
const guildDB = await fetchGuild(guild);
// If user is not on timeout
if (isTimeout) {
logger?.silly(`User ${user?.id} is on timeout`);
return interaction.editReply({
embeds: [
embed
.setDescription(
`You are on timeout, please wait ${guildDB?.credits.workTimeout} seconds.`
)
.setColor(errorColor),
],
});
}
await cooldown.command(interaction, guildDB?.credits?.workTimeout);
const creditsEarned = chance.integer({
min: 0,
@ -93,23 +73,5 @@ export default {
],
});
});
// Create a timeout for the user
await timeoutSchema?.create({
guildId: guild?.id,
userId: user?.id,
timeoutId: "2022-03-15-19-16",
});
setTimeout(async () => {
logger?.silly(`Removing timeout for user ${user?.id}`);
// When timeout is out, remove it from the database
await timeoutSchema?.deleteOne({
guildId: guild?.id,
userId: user?.id,
timeoutId: "2022-03-15-19-16",
});
}, guildDB?.credits?.workTimeout);
},
};

View file

@ -0,0 +1,22 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { CommandInteraction } from "discord.js";
import modules from "./modules";
export const moduleData = modules;
export const builder = new SlashCommandBuilder()
.setName("dns")
.setDescription("DNS commands.")
.addSubcommand(modules.lookup.builder);
export const execute = async (interaction: CommandInteraction) => {
switch (interaction.options.getSubcommand()) {
case "lookup":
return modules.lookup.execute(interaction);
default:
throw new Error(
`Unknown subcommand: ${interaction.options.getSubcommand()}`
);
}
};

View file

@ -0,0 +1,3 @@
import lookup from "./lookup";
export default { lookup };

View file

@ -0,0 +1,131 @@
import axios from "axios";
import { CommandInteraction, MessageEmbed } from "discord.js";
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
export default {
metadata: { guildOnly: false, ephemeral: false },
builder: (command: SlashCommandSubcommandBuilder) => {
return command
.setName("lookup")
.setDescription(
"Lookup a domain or ip. (Request sent over HTTP, proceed with caution!)"
)
.addStringOption((option) =>
option
.setName("query")
.setDescription("The query you want to look up.")
.setRequired(true)
);
},
execute: async (interaction: CommandInteraction) => {
const { errorColor, successColor, footerText, footerIcon } =
await getEmbedConfig(interaction.guild);
const embedTitle = "[:hammer:] Utility (Lookup)";
const { options } = interaction;
const query = options.getString("query");
await axios
.get(`http://ip-api.com/json/${query}`)
.then(async (response) => {
if (response.data.status !== "success") {
await interaction.editReply({
embeds: [
new MessageEmbed()
.setTitle(embedTitle)
.setFooter({
text: footerText,
iconURL: footerIcon,
})
.setTimestamp(new Date())
.setColor(errorColor)
.setFooter({ text: footerText, iconURL: footerIcon })
.setDescription(
`${response?.data?.message}: ${response?.data?.query}`
),
],
});
return;
}
await interaction.editReply({
embeds: [
new MessageEmbed()
.setTitle(embedTitle)
.setFooter({
text: footerText,
iconURL: footerIcon,
})
.setTimestamp(new Date())
.setColor(successColor)
.setFields([
{
name: ":classical_building: AS",
value: `${response.data.as || "Unknown"}`,
inline: true,
},
{
name: ":classical_building: ISP",
value: `${response.data.isp || "Unknown"}`,
inline: true,
},
{
name: ":classical_building: Organization",
value: `${response.data.org || "Unknown"}`,
inline: true,
},
{
name: ":compass: Latitude",
value: `${response.data.lat || "Unknown"}`,
inline: true,
},
{
name: ":compass: Longitude",
value: `${response.data.lon || "Unknown"}`,
inline: true,
},
{
name: ":clock4: Timezone",
value: `${response.data.timezone || "Unknown"}`,
inline: true,
},
{
name: ":globe_with_meridians: Country",
value: `${response.data.country || "Unknown"}`,
inline: true,
},
{
name: ":globe_with_meridians: Region",
value: `${response.data.regionName || "Unknown"}`,
inline: true,
},
{
name: ":globe_with_meridians: City",
value: `${response.data.city || "Unknown"}`,
inline: true,
},
{
name: ":globe_with_meridians: Country Code",
value: `${response.data.countryCode || "Unknown"}`,
inline: true,
},
{
name: ":globe_with_meridians: Region Code",
value: `${response.data.region || "Unknown"}`,
inline: true,
},
{
name: ":globe_with_meridians: ZIP",
value: `${response.data.zip || "Unknown"}`,
inline: true,
},
]),
],
});
});
},
};

View file

@ -0,0 +1,23 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { CommandInteraction } from "discord.js";
import logger from "../../../logger";
import modules from "../../commands/fun/modules";
export const builder = new SlashCommandBuilder()
.setName("fun")
.setDescription("Fun commands.")
.addSubcommand(modules.meme.builder);
export const moduleData = modules;
export const execute = async (interaction: CommandInteraction) => {
const { options } = interaction;
if (options.getSubcommand() === "meme") {
await modules.meme.execute(interaction);
} else {
logger.silly(`Unknown subcommand ${options.getSubcommand()}`);
}
};

View file

@ -0,0 +1,5 @@
import meme from "./meme";
export default {
meme,
};

View file

@ -0,0 +1,59 @@
import getEmbedConfig from "../../../../../helpers/getEmbedConfig";
import axios from "axios";
import { CommandInteraction, MessageEmbed } from "discord.js";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
export default {
metadata: { guildOnly: false, ephemeral: false, cooldown: 15 },
builder: (command: SlashCommandSubcommandBuilder) => {
return command.setName("meme").setDescription("Get a meme from r/memes)");
},
execute: async (interaction: CommandInteraction) => {
const { guild } = interaction;
const embedConfig = await getEmbedConfig(guild);
await axios
.get("https://www.reddit.com/r/memes/random/.json")
.then(async (res) => {
const response = res.data[0].data.children;
const content = response[0].data;
const embed = new MessageEmbed()
.setAuthor({
name: content.title,
iconURL:
"https://www.redditinc.com/assets/images/site/reddit-logo.png",
url: `https://reddit.com${content.permalink}`,
})
.setTitle("[:sweat_smile:] Meme")
.addFields([
{
name: "Author",
value: `[${content.author}](https://reddit.com/user/${content.author})`,
inline: true,
},
{
name: "Votes",
value: `${content.ups}/${content.downs}`,
inline: true,
},
])
.setTimestamp(new Date())
.setImage(content.url)
.setFooter({
text: embedConfig.footerText,
iconURL: embedConfig.footerIcon,
})
.setColor(embedConfig.successColor);
return interaction.editReply({ embeds: [embed] });
})
.catch((error) => {
throw new Error(error.message);
});
},
};

Some files were not shown because too many files have changed in this diff Show more