From 2073e6eb14247cd17254c0635407f9b80d46626b Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 5 Jul 2024 00:20:16 +1200 Subject: [PATCH] Added database files. --- src/database/mongoose.js | 32 ++++++ src/database/schemas/AutomodLogs.js | 39 +++++++ src/database/schemas/Giveaways.js | 64 +++++++++++ src/database/schemas/Guild.js | 153 ++++++++++++++++++++++++++ src/database/schemas/Member.js | 72 ++++++++++++ src/database/schemas/MemberStats.js | 66 +++++++++++ src/database/schemas/ModLog.js | 78 +++++++++++++ src/database/schemas/ReactionRoles.js | 92 ++++++++++++++++ src/database/schemas/Suggestions.js | 70 ++++++++++++ src/database/schemas/TranslateLog.js | 45 ++++++++ src/database/schemas/User.js | 72 ++++++++++++ 11 files changed, 783 insertions(+) create mode 100644 src/database/mongoose.js create mode 100644 src/database/schemas/AutomodLogs.js create mode 100644 src/database/schemas/Giveaways.js create mode 100644 src/database/schemas/Guild.js create mode 100644 src/database/schemas/Member.js create mode 100644 src/database/schemas/MemberStats.js create mode 100644 src/database/schemas/ModLog.js create mode 100644 src/database/schemas/ReactionRoles.js create mode 100644 src/database/schemas/Suggestions.js create mode 100644 src/database/schemas/TranslateLog.js create mode 100644 src/database/schemas/User.js diff --git a/src/database/mongoose.js b/src/database/mongoose.js new file mode 100644 index 0000000..28463c4 --- /dev/null +++ b/src/database/mongoose.js @@ -0,0 +1,32 @@ +const mongoose = require("mongoose"); +const { log, success, error } = require("../helpers/Logger"); + +mongoose.set("strictQuery", true); + +module.exports = { + async initializeMongoose() { + log(`Connecting to MongoDb...`); + + try { + await mongoose.connect(process.env.MONGO_CONNECTION); + + success("Mongoose: Database connection established"); + + return mongoose.connection; + } catch (err) { + error("Mongoose: Failed to connect to database", err); + process.exit(1); + } + }, + + schemas: { + Giveaways: require("./schemas/Giveaways"), + Guild: require("./schemas/Guild"), + Member: require("./schemas/Member"), + ReactionRoles: require("./schemas/ReactionRoles").model, + ModLog: require("./schemas/ModLog").model, + TranslateLog: require("./schemas/TranslateLog").model, + User: require("./schemas/User"), + Suggestions: require("./schemas/Suggestions").model, + }, +}; \ No newline at end of file diff --git a/src/database/schemas/AutomodLogs.js b/src/database/schemas/AutomodLogs.js new file mode 100644 index 0000000..2f56be5 --- /dev/null +++ b/src/database/schemas/AutomodLogs.js @@ -0,0 +1,39 @@ +const mongoose = require("mongoose"); + +const reqString = { + type: String, + required: true, +} + +const Schema = new mongoose.Schema( + { + guild_id: reqString, + member_id: reqString, + content: String, + reason: String, + strikes: Number, + }, + { + versionKey: false, + autoIndex: false, + timestamps: { + createdAt: "created_at", + updatedAt: false, + }, + } +); + +const Model = mongoose.Model("automod-logs", Schema); + +module.exports = { + addAutoModLogToDb: async (member, content, reason, strikes) => { + if (!member) throw new Error("Member is undefined"); + await new Model({ + guild_id: member.guild.id, + member_id: member.id, + content, + reason, + strikes, + }).save(); + }, +}; \ No newline at end of file diff --git a/src/database/schemas/Giveaways.js b/src/database/schemas/Giveaways.js new file mode 100644 index 0000000..1071978 --- /dev/null +++ b/src/database/schemas/Giveaways.js @@ -0,0 +1,64 @@ +const mongoose = require("mongoose"); + +const Schema = new mongoose.Schema( + { + messageId: String, + channelId: String, + guildId: String, + startAt: Number, + endAt: Number, + ended: Boolean, + winnerCount: Number, + prize: String, + messages: { + giveaway: String, + giveawayEnded: String, + inviteToParticipate: String, + drawing: String, + dropMessage: String, + winMessage: mongoose.Mixed, + embedFooter: mongoose.Mixed, + noWinner: String, + winners: String, + endedAt: String, + hostedBy: String, + }, + thumbnail: String, + hostedBy: String, + winnerIds: { type: [String], default: undefined }, + reaction: mongoose.Mixed, + botsCanWin: Boolean, + embedColor: mongoose.Mixed, + embedColorEnd: mongoose.Mixed, + exemptPermissions: { type: [], default: undefined }, + exemptMembers: String, + bonusEntries: String, + extraData: mongoose.Mixed, + lastChance: { + enabled: Boolean, + content: String, + threshold: Number, + embedColor: mongoose.Mixed, + }, + pauseOptions: { + isPaused: Boolean, + content: String, + unPauseAfter: Number, + embedColor: mongoose.Mixed, + durationAfterPause: Number, + }, + isDrop: Boolean, + allowedMentions: { + parse: { type: [String], default: undefined }, + users: { type: [String], default: undefined }, + roles: { type: [String], default: undefined }, + }, + }, + { + id: false, + autoIndex: false, + } +); + +const Model = mongoose.model("giveaways", Schema); +module.exports = Model; \ No newline at end of file diff --git a/src/database/schemas/Guild.js b/src/database/schemas/Guild.js new file mode 100644 index 0000000..55b16f2 --- /dev/null +++ b/src/database/schemas/Guild.js @@ -0,0 +1,153 @@ +const mongoose = require("mongoose"); +const { CACHE_SIZE, PREFIX_COMMANDS, STATS } = require("@root/config.js"); +const FixedSizeMap = require("fixedsize-map"); +const { getUser } = require("./User"); + +const cache = new FixedSizeMap(CACHE_SIZE.GUILDS); + +const Schema = new mongoose.Schema({ + _id: String, + data: { + name: String, + region: String, + owner: { type: String, ref: "users" }, + joinedAt: Date, + leftAt: Date, + bots: { type: Number, default: 0 }, + }, + prefix: { type: String, default: PREFIX_COMMANDS.DEFAULT_PREFIX }, + stats: { + enabled: Boolean, + xp: { + message: { type: String, default: STATS.DEFAULT_LVL_UP_MSG }, + channel: String, + }, + }, + ticket: { + log_channel: String, + limit: { type: Number, default: 10 }, + categories: [ + { + _id: false, + name: String, + staff_roles: [String], + }, + ], + }, + automod: { + debug: Boolean, + strikes: { type: Number, default: 10 }, + action: { type: String, default: "TIMEOUT" }, + wh_channels: [String], + anti_attachments: Boolean, + anti_invites: Boolean, + anti_links: Boolean, + anti_spam: Boolean, + anti_ghostping: Boolean, + anti_massmention: Number, + max_lines: Number, + }, + invite: { + tracking: Boolean, + ranks: [ + { + invites: { type: Number, required: true }, + _id: { type: String, required: true }, + }, + ], + }, + flag_translation: { + enabled: Boolean, + }, + modlog_channel: String, + max_warn: { + action: { + type: String, + enum: ["TIMEOUT", "KICK", "BAN"], + default: "KICK", + }, + limit: { type: Number, default: 5 }, + }, + counters: [ + { + _id: false, + counter_type: String, + name: String, + channel_id: String, + }, + ], + welcome: { + enabled: Boolean, + channel: String, + content: String, + embed: { + description: String, + color: String, + thumbnail: Boolean, + footer: String, + image: String, + }, + }, + farewell: { + enabled: Boolean, + channel: String, + content: String, + embed: { + description: String, + color: String, + thumbnail: Boolean, + footer: String, + image: String, + }, + }, + autorole: String, + suggestions: { + enabled: Boolean, + channel_id: String, + approved_channel: String, + rejected_channel: String, + staff_roles: [String], + }, +}); + +const Model = mongoose.model("guild", Schema); + +module.exports = { + /** + * @param {import('discord.js').Guild} guild + */ + getSettings: async (guild) => { + if (!guild) throw new Error("Guild is undefined"); + if (!guild.id) throw new Error("Guild Id is undefined"); + + const cached = cache.get(guild.id); + if (cached) return cached; + + let guildData = await Model.findById(guild.id); + if (!guildData) { + // save owner details + guild + .fetchOwner() + .then(async (owner) => { + const userDb = await getUser(owner); + await userDb.save(); + }) + .catch((ex) => {}); + + // create a new guild model + guildData = new Model({ + _id: guild.id, + data: { + name: guild.name, + region: guild.preferredLocale, + owner: guild.ownerId, + joinedAt: guild.joinedAt, + }, + }); + + await guildData.save(); + } + cache.add(guild.id, guildData); + return guildData; + }, +}; \ No newline at end of file diff --git a/src/database/schemas/Member.js b/src/database/schemas/Member.js new file mode 100644 index 0000000..49396bf --- /dev/null +++ b/src/database/schemas/Member.js @@ -0,0 +1,72 @@ +const mongoose = require("mongoose"); +const { CACHE_SIZE } = require("@root/config.js"); +const FixedSizeMap = require("fixedsize-map"); + +const cache = new FixedSizeMap(CACHE_SIZE.MEMBERS); + +const ReqString = { + type: String, + required: true, +}; + +const Schema = new mongoose.Schema( + { + guild_id: ReqString, + member_id: ReqString, + strikes: { type: Number, default: 0 }, + warnings: { type: Number, default: 0 }, + invite_data: { + inviter: String, + code: String, + tracked: { type: Number, default: 0 }, + fake: { type: Number, default: 0 }, + left: { type: Number, default: 0 }, + added: { type: Number, default: 0 }, + }, + }, + { + timestamps: { + createdAt: "created_at", + updatedAt: "updated_at", + }, + } +); + +const Model = mongoose.model("members", Schema); + +module.exports = { + getMember: async (guildId, memberId) => { + const key = `${guildId}|${memberId}`; + if (cache.contains(key)) return cache.get(key); + + let member = await Model.findOne({ guild_id: guildId, member_id: memberId }); + if (!member) { + member = new Model({ + guild_id: guildId, + member_id: memberId, + }); + } + + cache.add(key, member); + return member; + }, + + getInvitesLb: async (guildId, limit = 10) => + Model.aggregate([ + { $match: { guild_id: guildId } }, + { + $project: { + member_id: "$member_id", + invites: { + $subtract: [ + { $add: ["$invite_data.tracked", "$invite_data.added"] }, + { $add: ["$invite_data.left", "$invite_data.fake"] }, + ], + }, + }, + }, + { $match: { invites: { $gt: 0 } } }, + { $sort: { invites: -1 } }, + { $limit: limit }, + ]), +}; \ No newline at end of file diff --git a/src/database/schemas/MemberStats.js b/src/database/schemas/MemberStats.js new file mode 100644 index 0000000..a04d893 --- /dev/null +++ b/src/database/schemas/MemberStats.js @@ -0,0 +1,66 @@ +const mongoose = require("mongoose"); +const { CACHE_SIZE } = require("@root/config.js"); +const FixedSizeMap = require("fixedsize-map"); + +const cache = new FixedSizeMap(CACHE_SIZE.MEMBERS); + +const ReqString = { + type: String, + required: true, +}; + +const Schema = new mongoose.Schema( + { + guild_id: ReqString, + member_id: ReqString, + messages: { type: Number, default: 0 }, + voice: { + connections: { type: Number, default: 0 }, + time: { type: Number, default: 0 }, + }, + commands: { + prefix: { type: Number, default: 0 }, + slash: { type: Number, default: 0 }, + }, + contexts: { + message: { type: Number, default: 0 }, + user: { type: Number, default: 0 }, + }, + xp: { type: Number, default: 0 }, + level: { type: Number, default: 1 }, + }, + { + timestamps: { + createdAt: "created_at", + updatedAt: "updated_at", + }, + } +); + +const Model = mongoose.model("member-stats", Schema); + +module.exports = { + getMemberStats: async (guildId, memberId) => { + const key = `${guildId}|${memberId}`; + if (cache.contains(key)) return cache.get(key); + + let member = await Model.findOne({ guild_id: guildId, member_id: memberId }); + if (!member) { + member = new Model({ + guild_id: guildId, + member_id: memberId, + }); + } + + cache.add(key, member); + return member; + }, + + getXpLb: async (guildId, limit = 10) => + Model.find({ + guild_id: guildId, + }) + .limit(limit) + .sort({ level: -1, xp: -1 }) + .lean(), +}; \ No newline at end of file diff --git a/src/database/schemas/ModLog.js b/src/database/schemas/ModLog.js new file mode 100644 index 0000000..05787d6 --- /dev/null +++ b/src/database/schemas/ModLog.js @@ -0,0 +1,78 @@ +const mongoose = require("mongoose"); + +const reqString = { + type: String, + required: true, +}; + +const Schema = new mongoose.Schema( + { + guild_id: reqString, + member_id: String, + reason: String, + admin: { + id: reqString, + tag: reqString, + }, + type: { + type: String, + required: true, + enum: [ + "PURGE", + "WARN", + "TIMEOUT", + "UNTIMEOUT", + "KICK", + "SOFTBAN", + "BAN", + "UNBAN", + "VMUTE", + "VUNMUTE", + "DEAFEN", + "UNDEAFEN", + "DISCONNECT", + "MOVE", + ], + }, + }, + { + versionKey: false, + autoIndex: false, + timestamps: { + createdAt: "created_at", + updatedAt: false, + }, + } +); + +const Model = mongoose.model("mod-logs", Schema); + +module.exports = { + model: Model, + + addModLogToDb: async (admin, target, reason, type) => + await new Model({ + guild_id: admin.guild.id, + member_id: target.id, + reason, + admin: { + id: admin.id, + tag: admin.user.tag, + }, + type, + }).save(), + + getWarningLogs: async (guildId, targetId) => + Model.find({ + guild_id: guildId, + member_id: targetId, + type: "WARN", + }).lean(), + + clearWarningLogs: async (guildId, targetId) => + Model.deleteMany({ + guild_id: guildId, + member_id: targetId, + type: "WARN", + }), +}; \ No newline at end of file diff --git a/src/database/schemas/ReactionRoles.js b/src/database/schemas/ReactionRoles.js new file mode 100644 index 0000000..2f44291 --- /dev/null +++ b/src/database/schemas/ReactionRoles.js @@ -0,0 +1,92 @@ +const mongoose = require("mongoose"); + +const reqString = { + type: String, + required: true, +}; + +const Schema = new mongoose.Schema( + { + guild_id: reqString, + channel_id: reqString, + message_id: reqString, + roles: [ + { + _id: false, + emote: reqString, + role_id: reqString, + }, + ], + }, + { + timestamps: { + createdAt: "created_at", + updatedAt: false, + }, + } +); + +const Model = mongoose.model("reaction-roles", Schema); + +// Cache +const rrCache = new Map(); +const getKey = (guildId, channelId, messageId) => `${guildId}|${channelId}|${messageId}`; + +module.exports = { + model: Model, + + cacheReactionRoles: async (client) => { + // clear previous cache + rrCache.clear(); + + // load all docs from database + const docs = await Model.find().lean(); + + // validate and cache docs + for (const doc of docs) { + const guild = client.guilds.cache.get(doc.guild_id); + if (!guild) { + // await Model.deleteMany({ guild_id: doc.guild_id }); + continue; + } + if (!guild.channels.cache.has(doc.channel_id)) { + // await Model.deleteMany({ guild_id: doc.guild_id, channel_id: doc.channel_id }); + continue; + } + const key = getKey(doc.guild_id, doc.channel_id, doc.message_id); + rrCache.set(key, doc.roles); + } + }, + + getReactionRoles: (guildId, channelId, messageId) => rrCache.get(getKey(guildId, channelId, messageId)) || [], + + addReactionRole: async (guildId, channelId, messageId, emote, roleId) => { + const filter = { guild_id: guildId, channel_id: channelId, message_id: messageId }; + + // Pull if existing configuration is present + await Model.updateOne(filter, { $pull: { roles: { emote } } }); + + const data = await Model.findOneAndUpdate( + filter, + { + $push: { + roles: { emote, role_id: roleId }, + }, + }, + { upsert: true, new: true } + ).lean(); + + // update cache + const key = getKey(guildId, channelId, messageId); + rrCache.set(key, data.roles); + }, + + removeReactionRole: async (guildId, channelId, messageId) => { + await Model.deleteOne({ + guild_id: guildId, + channel_id: channelId, + message_id: messageId, + }); + rrCache.delete(getKey(guildId, channelId, messageId)); + }, +}; \ No newline at end of file diff --git a/src/database/schemas/Suggestions.js b/src/database/schemas/Suggestions.js new file mode 100644 index 0000000..2e7e979 --- /dev/null +++ b/src/database/schemas/Suggestions.js @@ -0,0 +1,70 @@ +const mongoose = require("mongoose"); + +const Schema = new mongoose.Schema( + { + guild_id: String, + channel_id: String, + message_id: String, + user_id: String, + suggestion: String, + status: { + type: String, + enum: ["PENDING", "APPROVED", "REJECTED", "DELETED"], + default: "PENDING", + }, + stats: { + upvotes: { type: Number, default: 0 }, + downvotes: { type: Number, default: 0 }, + }, + status_updates: [ + { + _id: false, + user_id: String, + status: { + type: String, + enum: ["APPROVED", "REJECTED", "DELETED"], + }, + reason: String, + timestamp: { type: Date, default: new Date() }, + }, + ], + }, + { + timestamps: { + createdAt: "created_at", + updatedAt: "updated_at", + }, + } +); + +const Model = mongoose.model("suggestions", Schema); + +module.exports = { + model: Model, + + addSuggestion: async (message, userId, suggestion) => { + return new Model({ + guild_id: message.guildId, + channel_id: message.channelId, + message_id: message.id, + user_id: userId, + suggestion: suggestion, + }).save(); + }, + + findSuggestion: async (guildId, messageId) => { + return Model.findOne({ guild_id: guildId, message_id: messageId }); + }, + + deleteSuggestionDb: async (guildId, messageId, memberId, reason) => { + return Model.updateOne( + { guild_id: guildId, message_id: messageId }, + { + status: "DELETED", + $push: { + status_updates: { user_id: memberId, status: "DELETED", reason }, + }, + } + ); + }, +}; \ No newline at end of file diff --git a/src/database/schemas/TranslateLog.js b/src/database/schemas/TranslateLog.js new file mode 100644 index 0000000..2b946f0 --- /dev/null +++ b/src/database/schemas/TranslateLog.js @@ -0,0 +1,45 @@ +const mongoose = require("mongoose"); + +const reqString = { + type: String, + required: true, +}; + +const Schema = new mongoose.Schema( + { + guild_id: reqString, + channel_id: reqString, + message_id: reqString, + emoji: reqString, + }, + { + versionKey: false, + autoIndex: false, + timestamps: { + createdAt: "created_at", + updatedAt: false, + }, + } +); + +const Model = mongoose.model("logs-translation", Schema); + +module.exports = { + model: Model, + + isTranslated: async (message, code) => + Model.findOne({ + guild_id: message.guildId, + channel_id: message.channelId, + message_id: message.id, + emoji: code, + }).lean(), + + logTranslation: async (message, code) => + new Model({ + guild_id: message.guildId, + channel_id: message.channelId, + message_id: message.id, + emoji: code, + }).save(), +}; \ No newline at end of file diff --git a/src/database/schemas/User.js b/src/database/schemas/User.js new file mode 100644 index 0000000..3022d5b --- /dev/null +++ b/src/database/schemas/User.js @@ -0,0 +1,72 @@ +const mongoose = require("mongoose"); +const { CACHE_SIZE } = require("@root/config.js"); +const FixedSizeMap = require("fixedsize-map"); + +const cache = new FixedSizeMap(CACHE_SIZE.USERS); + +const Schema = new mongoose.Schema( + { + _id: String, + username: String, + discriminator: String, + logged: Boolean, + coins: { type: Number, default: 0 }, + bank: { type: Number, default: 0 }, + reputation: { + received: { type: Number, default: 0 }, + given: { type: Number, default: 0 }, + timestamp: Date, + }, + daily: { + streak: { type: Number, default: 0 }, + timestamp: Date, + }, + }, + { + timestamps: { + createdAt: "created_at", + updatedAt: "updated_at", + }, + } +); + +const Model = mongoose.model("user", Schema); + +module.exports = { + /** + * @param {import('discord.js').User} user + */ + getUser: async (user) => { + if (!user) throw new Error("User is required."); + if (!user.id) throw new Error("User Id is required."); + + const cached = cache.get(user.id); + if (cached) return cached; + + let userDb = await Model.findById(user.id); + if (!userDb) { + userDb = new Model({ + _id: user.id, + username: user.username, + discriminator: user.discriminator, + }); + } + + // Temporary fix for users who where added to DB before v5.0.0 + // Update username and discriminator in previous DB + else if (!userDb.username || !userDb.discriminator) { + userDb.username = user.username; + userDb.discriminator = user.discriminator; + } + + cache.add(user.id, userDb); + return userDb; + }, + + getReputationLb: async (limit = 10) => { + return Model.find({ "reputation.received": { $gt: 0 } }) + .sort({ "reputation.received": -1, "reputation.given": 1 }) + .limit(limit) + .lean(); + }, +}; \ No newline at end of file