const { Observer } = require('../../../interfaces/'); const { Util } = require('../../../../util'); const CONSTANTS = { COLORS: { RED: 16711680, // message delete YELLOW: 15120384, // message edit LIGHT_BLUE: 11337726, // message pin BLUE: 479397 }, IMAGES: { PREMIUM_LIMIT: 2, UPLOAD_LIMIT: { '0': 8, '1': 8, '2': 50, '3': 100 }, MB_DIVIDER: 1024*1024 }, WEEK: 7 * 24 * 60 * 60 }; class GuildLogger extends Observer { constructor(client) { super(client, { name: 'guildLogger', priority: 3, disabled: true }); this.hooks = [ ['message', this.storeAttachment.bind(this)], //Attachment logging ['messageDelete', this.messageDelete.bind(this)], ['messageDeleteBulk', this.messageDeleteBulk.bind(this)], ['messageUpdate', this.messageEdit.bind(this)], ['voiceStateUpdate', this.voiceState.bind(this)], ['guildBanAdd', this.ban.bind(this)], ['guildBanRemove', this.unban.bind(this)], ['guildMemberAdd', this.memberJoin.bind(this)], ['guildMemberRemove', this.memberLeave.bind(this)], ['guildMemberUpdate', this.memberUpdate.bind(this)] ]; } async storeAttachment(message) { const { guild, member, author, channel } = message; if(!guild || author.bot) return; if(!message.attachments.size) return; console.log('Store call'); const settings = await guild.settings(), setting = settings.messageLog, roles = member.roles.cache.map((r) => r.id), { ignoredRoles } = setting; //if(guild._settings.premium < CONSTANTS.IMAGES.PREMIUM_LIMIT) return; if(!setting.attachments || setting.ignoredChannels.includes(channel.id)) return; if(setting.ignoredRoles.length && roles.length) for(const role of ignoredRoles) if(roles.includes(role)) return; console.log('Can store'); const attachments = message.attachments.values(); for(const attachment of attachments) { const TH = this.client.transactionHandler; const { size, name, id } = attachment; const fsize = size/CONSTANTS.IMAGES.MB_DIVIDER; // File size in MB if(fsize > CONSTANTS.IMAGES.UPLOAD_LIMIT[guild.premiumTier]) continue; const buffer = await Util.downloadAsBuffer(attachment.url).catch((err) => { this.client.logger.error(err); return null; }); if(!buffer) return; const data = { buffer, id }; try { //TODO: test this //const start = Date.now(); const result = await TH.send({ provider: 'mongodb', request: { type: 'insertOne', collection: 'attachment_logs', data } }); //console.log(`Took ${Date.now()-start}ms to insert image.`); //console.log(result); const metadata = { database_ID: result.insertedId, id, guild: guild.id, message: message.id, author: author.id, name, size, timestamp: Math.floor(Date.now()/1000), removeAt: Math.floor(Date.now()/1000 + guild._settings.premium * CONSTANTS.WEEK * 0.25) }; //console.log(metadata); await TH.send({ provider: 'mongodb', request: { type: 'insertOne', collection: 'attachment_logs_index', data: metadata } }); //console.log(`Took ${Date.now()-start}ms to insert entire entry.`); } catch (err) { this.client.logger.error('Something went wrong with storing image to db:\n' + err.stack); } } } //TODO: Figure this thing out, this should be called from messageDelete and rawMessageDelete if any attachments are present //data should be an object containing the necessary information to query for the attachment from the db, and the relevant information to log it //Will figure this out once I get to that point async logAttachment(data) { } async messageDelete(message) { if(message.author.bot) return; const { guild } = message; if (!guild) return; if (!message.member) message.member = await guild.members.fetch(message.author); const { member, channel, author } = message; const settings = await guild.settings(); const chatlogs = settings.messageLog; if (!chatlogs || !chatlogs.channel) return; const { ignoredRoles, ignoredChannels } = chatlogs; const logChannel = await guild.resolveChannel(chatlogs.channel); if(!logChannel) return; const perms = logChannel.permissionsFor(guild.me); if(!perms.has('SEND_MESSAGES') || !perms.has('VIEW_CHANNEL')) return; if (ignoredRoles.length && member.roles.cache.size) { const roles = member.roles.cache.map((r) => r.id); for (const role of ignoredRoles) { if (roles.includes(role)) return; } } if (ignoredChannels && ignoredChannels.includes(channel.id)) return; if (message.attachments.size) return this.logAttachment({ msgID: message.id, guildID: guild.id, channelID: channel.id, logChannel }); const embed = { title: message.format('MSGLOG_DELETE_TITLE', { author: Util.escapeMarkdown(author.tag), channel: channel.name }), description: message.content, footer: { text: message.format('MSGLOG_DELETE_FOOTER', { msgID: message.id, userID: author.id }) }, color: CONSTANTS.COLORS.RED, timestamp: message.createdAt }; await logChannel.send({ embed }); } async messageDeleteBulk(messages) { } async messageEdit(oldMessage, newMessage) { // embeds loading in (ex. when a link is posted) would cause a message edit event if(oldMessage.embeds.length !== newMessage.embeds.length) return; if(oldMessage.author.bot) return; const { guild } = oldMessage; if (!guild) return; if (!oldMessage.member) oldMessage.member = await guild.members.fetch(oldMessage.author); const { member, channel, author } = oldMessage; const settings = await guild.settings(); const chatlogs = settings.messageLog; if (!chatlogs.channel) return; const { ignoredRoles, ignoredChannels } = chatlogs; const logChannel = await guild.resolveChannel(chatlogs.channel); if(!logChannel) return; const perms = logChannel.permissionsFor(guild.me); if(!perms.has('SEND_MESSAGES') || !perms.has('VIEW_CHANNEL')) return; if (ignoredRoles && member.roles.cache.size) { const roles = member.roles.cache.map((r) => r.id); for (const role of ignoredRoles) { if (roles.includes(role)) return; } } if (ignoredChannels && ignoredChannels.includes(channel.id)) return; if(oldMessage.content === newMessage.content && oldMessage.pinned !== newMessage.pinned) { const embed = { title: oldMessage.format('MSGLOG_PINNED_TITLE', { author: Util.escapeMarkdown(author.tag), channel: channel.name, pinned: oldMessage.format('PIN_TOGGLE', { toggle: newMessage.pinned }, true) }), description: oldMessage.format('MSGLOG_EDIT_JUMP', { guild: guild.id, channel: channel.id, message: oldMessage.id }), color: CONSTANTS.COLORS.LIGHT_BLUE }; if(oldMessage.content.length) embed.description += '\n' + oldMessage.content.substring(0, 1900); if(oldMessage.attachments.size) { const img = oldMessage.attachments.first(); if(img.height && img.width) embed.image = { url: img.url }; } await logChannel.send({ embed }).catch(this.client.logger.error); } else { const embed = { title: oldMessage.format('MSGLOG_EDIT_TITLE', { author: Util.escapeMarkdown(author.tag), channel: channel.name }), footer: { text: oldMessage.format('MSGLOG_EDIT_FOOTER', { msgID: oldMessage.id, userID: author.id }) }, description: oldMessage.format('MSGLOG_EDIT_JUMP', { guild: guild.id, channel: channel.id, message: oldMessage.id }), color: CONSTANTS.COLORS.YELLOW, timestamp: oldMessage.createdAt, fields: [ // { // name: oldMessage.format('MSGLOG_EDIT_OLD'), // value: oldMessage.content.length > 1024 ? oldMessage.content.substring(0, 1021) + '...' : oldMessage.content // }, // { // name: oldMessage.format('MSGLOG_EDIT_NEW'), // value: newMessage.content.length > 1024 ? newMessage.content.substring(0, 1021) + '...' : newMessage.content // } ] }; const oldCon = oldMessage.content, newCon = newMessage.content; //Original content embed.fields.push({ name: oldMessage.format('MSGLOG_EDIT_OLD'), value: oldCon.length > 1024 ? oldCon.substring(0, 1021) + '...' : oldCon }); if(oldCon.length > 1024) embed.fields.push({ name: '\u200b', value: '...' + oldCon.substring(1021) }); //Edited content embed.fields.push({ name: oldMessage.format('MSGLOG_EDIT_NEW'), value: newCon.length > 1024 ? newCon.substring(0, 1021) + '...' : newCon }); if(newCon.length > 1024) embed.fields.push({ name: '\u200b', value: '...' + newCon.substring(1021) }); //if(oldMessage.content.length > 1024) embed.description += '\n' + oldMessage.format('MSGLOG_EDIT_OLD_CUTOFF'); //if(newMessage.content.length > 1024) embed.description += '\n' + oldMessage.format('MSGLOG_EDIT_NEW_CUTOFF'); await logChannel.send({ embed }).catch((err) => { this.client.logger.error('Error in message edit:\n' + err.stack); }); } } async voiceState(oldState, newState) { if(oldState.channel && newState.channel && oldState.channel === newState.channel) return; const { guild, member } = oldState; //TODO: add checks for disconnecting bot from vc when left alone in one (music player) const settings = await guild.settings(); const setting = settings.voiceLog; if(!setting || !setting.channel) return; const logChannel = await guild.resolveChannel(setting.channel); if(!logChannel) return; const perms = logChannel.permissionsFor(guild.me); if(!perms.has('SEND_MESSAGES') || !perms.has('VIEW_CHANNEL')) return; let index = null; const langParams = { nickname: member.nickname ? `\`(${member.nickname})\`` : '', tag: Util.escapeMarkdown(member.user.tag), id: member.id, oldChannel: oldState.channel?.name, newChannel: newState.channel?.name }; if(!oldState.channel && newState.channel) index = 'VCLOG_JOIN'; else if(oldState.channel && newState.channel) index = 'VCLOG_SWITCH'; else index = 'VCLOG_LEAVE'; this.client.rateLimiter.queueSend(logChannel, guild.format(index, langParams).trim()); } async ban(guild, user) { } async unban(guild, user) { } _replaceTags(text, member) { const { user, guild } = member; return text .replace(/\{mention\}/gu, `<@${member.id}>`) .replace(/\{tag\}/gu, Util.escapeMarkdown(user.tag)) .replace(/\{user\}/gu, Util.escapeMarkdown(user.username)) .replace(/\{guildsize\}/gu, guild.memberCount) .replace(/\{guildname\}/gu, guild.name) .replace(/\{accage\}/gu, this.client.resolver.timeAgo(Date.now()/1000 - user.createdTimestamp/1000)) //.replace(/a/, '1') .replace(/\{id\}/gu, user.id) .trim(); } async memberJoin(member) { const { guild } = member; const settings = await guild.settings(); const setting = settings.memberLog; if(!setting.channel) return; const logChannel = await guild.resolveChannel(setting.channel); if(!logChannel) return; const perms = logChannel.permissionsFor(guild.me); if(!perms.has('SEND_MESSAGES') || !perms.has('VIEW_CHANNEL')) return; let { joinMessage } = setting; joinMessage = this._replaceTags(joinMessage, member); this.client.rateLimiter.queueSend(logChannel, joinMessage); } async memberLeave(member) { const { guild } = member; const settings = await guild.settings(); const setting = settings.memberLog; if(!setting.channel) return; const logChannel = await guild.resolveChannel(setting.channel); if(!logChannel) return; const perms = logChannel.permissionsFor(guild.me); if(!perms.has('SEND_MESSAGES') || !perms.has('VIEW_CHANNEL')) return; let { leaveMessage } = setting; leaveMessage = this._replaceTags(leaveMessage, member); this.client.rateLimiter.queueSend(logChannel, leaveMessage); } async memberUpdate(oldMember, newMember) { if(oldMember.nickname === newMember.nickname) return; const { guild, user } = oldMember; const settings = await guild.settings(); const setting = settings.nicknameLog; if(!setting.channel) return; const logChannel = await guild.resolveChannel(setting.channel); if(!logChannel) return; const perms = logChannel.permissionsFor(guild.me); if(!perms.has('SEND_MESSAGES') || !perms.has('VIEW_CHANNEL')) return; const oldNick = oldMember.nickname || oldMember.user.username; const newNick = newMember.nickname || newMember.user.username; const embed = { title: guild.format('NICKLOG_TITLE', { user: Util.escapeMarkdown(user.tag) }), description: guild.format('NICKLOG_DESCRIPTION', { oldNick, newNick }), footer: { text: guild.format('NICKLOG_FOOTER', { id: user.id }) }, color: CONSTANTS.COLORS.BLUE }; logChannel.send({ embed }); } } module.exports = GuildLogger;