2021-06-20 13:11:36 +02:00
|
|
|
class ChannelHandler {
|
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
constructor (modmail, opts) {
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
this.modmail = modmail;
|
|
|
|
this.client = modmail.client;
|
|
|
|
|
|
|
|
this.awaitingChannel = {};
|
|
|
|
|
|
|
|
this.mainServer = null;
|
|
|
|
this.bansServer = null;
|
|
|
|
|
2021-07-09 14:29:17 +02:00
|
|
|
this.categories = opts.modmailCategory.map((id) => id.toString());
|
2021-06-20 13:11:36 +02:00
|
|
|
this.cache = modmail.cache;
|
|
|
|
|
|
|
|
this.graveyardInactive = opts.graveyardInactive;
|
|
|
|
this.readInactive = opts.readInactive;
|
|
|
|
this.channelSweepInterval = opts.channelSweepInterval;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
init () {
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
this.mainServer = this.modmail.mainServer;
|
|
|
|
this.bansServer = this.modmail.bansServer;
|
|
|
|
|
|
|
|
const { channels } = this.mainServer;
|
|
|
|
this.newMail = channels.resolve(this.categories[0]);
|
2021-07-09 14:29:17 +02:00
|
|
|
if (!this.newMail) this.client.logger.warn(`Missing new mail category!`);
|
2021-06-20 13:11:36 +02:00
|
|
|
this.readMail = channels.resolve(this.categories[1]);
|
2021-07-09 14:29:17 +02:00
|
|
|
if (!this.readMail) this.client.logger.warn(`Missing read mail category!`);
|
2021-06-20 13:11:36 +02:00
|
|
|
this.graveyard = channels.resolve(this.categories[2]);
|
2021-07-09 14:29:17 +02:00
|
|
|
if (!this.graveyard) this.client.logger.warn(`Missing graveyard category!`);
|
|
|
|
|
|
|
|
if (!this.newMail || !this.readMail || !this.graveyard) {
|
|
|
|
this.client.logger.debug(`Some mail categories were missing: ${this.categories}`);
|
|
|
|
}
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
// Sweep graveyard every x min and move stale channels to graveyard
|
|
|
|
this.sweeper = setInterval(this.sweepChannels.bind(this), this.channelSweepInterval * 60 * 1000);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
async send (target, embed, newEntry) {
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
// Load & update the users past modmails
|
|
|
|
const history = await this.cache.loadModmailHistory(target.id)
|
|
|
|
.catch((err) => {
|
|
|
|
this.client.logger.error(`Error during loading of past mail:\n${err.stack}`);
|
|
|
|
return { error: true };
|
|
|
|
});
|
|
|
|
if (history.error) return {
|
|
|
|
error: true,
|
|
|
|
msg: `Internal error, this has been logged.`
|
|
|
|
};
|
|
|
|
|
|
|
|
const channel = await this.load(target, history).catch(this.client.logger.error.bind(this.client.logger));
|
2021-06-21 16:54:20 +02:00
|
|
|
history.push(newEntry);
|
2021-06-20 13:11:36 +02:00
|
|
|
const sent = await channel.send({ embed }).catch((err) => {
|
|
|
|
this.client.logger.error(`channel.send errored:\n${err.stack}\nContent: "${embed}"`);
|
|
|
|
});
|
|
|
|
await channel.edit({ parentID: this.readMail.id, lockPermissions: true }).catch((err) => {
|
|
|
|
this.client.logger.error(`Error during channel transition:\n${err.stack}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (this.cache.queue.includes(target.id)) this.cache.queue.splice(this.cache.queue.indexOf(target.id), 1);
|
|
|
|
if (!this.cache.updatedThreads.includes(target.id)) this.cache.updatedThreads.push(target.id);
|
|
|
|
if (this.readMail.children.size > 45) this.sweepChannels({ count: 5, force: true });
|
|
|
|
|
|
|
|
return sent;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
async setReadState (target, channel, staff, state) {
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
const history = await this.cache.loadModmailHistory(target)
|
|
|
|
.catch((err) => {
|
|
|
|
this.client.logger.error(`Error during loading of past mail:\n${err.stack}`);
|
|
|
|
return { error: true };
|
|
|
|
});
|
|
|
|
if (history.error) return {
|
|
|
|
error: true,
|
|
|
|
msg: `Internal error, this has been logged.`
|
|
|
|
};
|
2021-06-20 16:43:05 +02:00
|
|
|
if (!history.length) return {
|
|
|
|
error: true,
|
|
|
|
msg: `User has no modmail history.`
|
|
|
|
};
|
2021-09-02 13:38:55 +02:00
|
|
|
if (history[history.length - 1].readState !== state) history.push({ author: staff.id, timestamp: Date.now(), readState: state }); // To keep track of read state
|
|
|
|
if (state === 'unread') await this.load(await this.client.resolveUser(target), history);
|
2021-06-20 13:11:36 +02:00
|
|
|
|
2021-09-02 13:38:55 +02:00
|
|
|
if (channel) await channel.edit({ parentID: state === 'read' ? this.readMail.id : this.newMail.id, lockPermissions: true });
|
2021-06-20 13:11:36 +02:00
|
|
|
if (!this.cache.updatedThreads.includes(target)) this.cache.updatedThreads.push(target);
|
2021-09-02 13:38:55 +02:00
|
|
|
if (this.cache.queue.includes(target) && state === 'read') this.cache.queue.splice(this.cache.queue.indexOf(target), 1);
|
|
|
|
else if (!this.cache.queue.includes(target)) this.cache.queue.push(target);
|
2021-06-20 13:11:36 +02:00
|
|
|
return {};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process channels for incoming modmail
|
|
|
|
*
|
|
|
|
* @param {GuildMember} member
|
|
|
|
* @param {string} id
|
|
|
|
* @param {Array<object>} history
|
|
|
|
* @return {TextChannel}
|
|
|
|
* @memberof ChannelHandler
|
|
|
|
*/
|
2021-10-22 09:35:04 +02:00
|
|
|
load (target, history) {
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
if (this.awaitingChannel[target.id]) return this.awaitingChannel[target.id];
|
|
|
|
// eslint-disable-next-line no-async-promise-executor
|
|
|
|
const promise = new Promise(async (resolve, reject) => {
|
|
|
|
|
|
|
|
const channelID = this.modmail.cache.channels[target.id];
|
|
|
|
const guild = this.mainServer;
|
|
|
|
const user = target.user || target;
|
|
|
|
const member = target.user ? target : null;
|
|
|
|
let channel = guild.channels.resolve(channelID);
|
|
|
|
const { context } = this.client._options;
|
|
|
|
|
|
|
|
if (this.newMail.children.size >= 45) this.overflow();
|
|
|
|
|
|
|
|
if (!channel) { // Create and populate channel
|
|
|
|
channel = await guild.channels.create(`${user.username}_${user.discriminator}`, {
|
|
|
|
parent: this.newMail.id
|
2022-01-24 21:15:25 +01:00
|
|
|
}).catch(err => {
|
|
|
|
this.client.logger.warn(`Failed to create channel for ${user.tag}, trying again.\n${err.stack || err}`);
|
2021-06-20 13:11:36 +02:00
|
|
|
});
|
2022-01-24 21:15:25 +01:00
|
|
|
if (!channel) channel = await guild.channels.create(`${user.id}`, {
|
|
|
|
parent: this.newMail.id
|
|
|
|
}).catch(err => {
|
|
|
|
// this.client.logger.error(`Failed on second create:\n${err.stack || err}`);
|
|
|
|
return { err };
|
|
|
|
});
|
|
|
|
if (!channel.err) return reject(channel.err);
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
// Start with user info embed
|
|
|
|
const embed = {
|
|
|
|
author: { name: user.tag },
|
|
|
|
thumbnail: {
|
|
|
|
url: user.displayAvatarURL({ dynamic: true })
|
|
|
|
},
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: '__User Data__',
|
2021-10-22 09:35:04 +02:00
|
|
|
value: `**User:** <@${user.id}>\n`
|
|
|
|
+ `**Account created:** ${user.createdAt.toDateString()}\n`,
|
2021-06-20 13:11:36 +02:00
|
|
|
inline: false
|
|
|
|
}
|
|
|
|
],
|
|
|
|
footer: { text: `• User ID: ${user.id}` },
|
|
|
|
color: guild.me.highestRoleColor
|
|
|
|
};
|
2021-07-19 17:44:28 +02:00
|
|
|
if (member && member.inAppealServer) {
|
2021-07-19 21:20:14 +02:00
|
|
|
if (guild.me.hasPermission('BAN_MEMBERS')) {
|
|
|
|
const ban = await guild.fetchBan(member.id).catch(() => null);
|
|
|
|
if (ban) embed.description = `**__USER IS BANNED FROM MAIN SERVER__**`;
|
|
|
|
else embed.description = `**__USER IS IN APPEAL SERVER BUT NOT BANNED FROM MAIN__**`;
|
|
|
|
} else embed.description = `**__USER IS IN APPEAL SERVER__**`;
|
2021-07-19 17:44:28 +02:00
|
|
|
} else if (member) embed.fields.push({
|
2021-06-20 13:11:36 +02:00
|
|
|
name: '__Member Data__',
|
2021-10-22 09:35:04 +02:00
|
|
|
value: `**Nickname:** ${member.nickname || 'N/A'}\n`
|
|
|
|
+ `**Server join date:** ${member.joinedAt.toDateString()}\n`
|
|
|
|
+ `**Roles:** ${member.roles.cache.filter((r) => r.id !== guild.roles.everyone.id).map((r) => `<@&${r.id}>`).join(' ')}`,
|
2021-06-20 13:11:36 +02:00
|
|
|
inline: false
|
|
|
|
});
|
|
|
|
|
2021-11-29 11:25:38 +01:00
|
|
|
await channel.send({ embed }).catch(err => this.client.logger.error(`ChannelHandler.load errored at channel.send:\n${err.stack}`));
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
// Load in context
|
|
|
|
const len = history.length;
|
|
|
|
for (let i = context < len ? context : len; i > 0; i--) {
|
|
|
|
const entry = history[len - i];
|
|
|
|
if (!entry) continue;
|
2021-10-22 09:34:48 +02:00
|
|
|
if ([ 'read', 'unread' ].includes(entry.readState)) continue;
|
2021-06-20 13:11:36 +02:00
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
// eslint-disable-next-line no-shadow
|
2021-06-20 13:11:36 +02:00
|
|
|
const user = await this.client.resolveUser(entry.author).catch(this.client.logger.error.bind(this.client.logger));
|
|
|
|
const mem = await this.modmail.getMember(user.id).catch(this.client.logger.error.bind(this.client.logger));
|
|
|
|
if (!user) return reject(new Error(`Failed to find user`));
|
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
// eslint-disable-next-line no-shadow
|
2021-06-20 13:11:36 +02:00
|
|
|
const embed = {
|
|
|
|
footer: {
|
|
|
|
text: user.id
|
|
|
|
},
|
|
|
|
author: {
|
2021-06-21 16:54:20 +02:00
|
|
|
// eslint-disable-next-line no-nested-ternary
|
|
|
|
name: user.tag + (entry.anon ? ' (ANON)' : entry.isReply ? ' (STAFF)' : ''),
|
2021-06-20 13:11:36 +02:00
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
icon_url: user.displayAvatarURL({ dynamic: true })
|
|
|
|
},
|
2021-06-22 22:59:12 +02:00
|
|
|
// eslint-disable-next-line no-nested-ternary
|
|
|
|
description: entry.content && entry.content.length ? entry.content.length > 2000 ? `${entry.content.substring(0, 2000)}...\n\n**Content cut off**` : entry.content : `**__MISSING CONTENT__**`,
|
2021-06-20 13:11:36 +02:00
|
|
|
color: mem?.highestRoleColor || 0,
|
|
|
|
fields: [],
|
|
|
|
timestamp: new Date(entry.timestamp)
|
|
|
|
};
|
|
|
|
|
|
|
|
if (entry.attachments && entry.attachments.length) embed.fields.push({
|
|
|
|
name: '__Attachments__',
|
|
|
|
value: entry.attachments.join('\n').substring(0, 1000)
|
|
|
|
});
|
|
|
|
|
2021-11-29 11:25:38 +01:00
|
|
|
await channel.send({ embed }).catch(err => this.client.logger.error(`ChannelHandler.load errored at channel.send:\n${err.stack}`));
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.modmail.cache.channels[user.id] = channel.id;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the right category
|
2021-10-22 09:35:04 +02:00
|
|
|
// if (channel.parentID !== this.newMail.id)
|
2021-06-20 13:11:36 +02:00
|
|
|
await channel.edit({ parentID: this.newMail.id, lockPermissions: true }).catch((err) => {
|
|
|
|
this.client.logger.error(`Error during channel transition:\n${err.stack}`);
|
|
|
|
});
|
|
|
|
delete this.awaitingChannel[user.id];
|
|
|
|
resolve(channel);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
this.awaitingChannel[target.id] = promise;
|
|
|
|
return promise;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param {object} { age, count, force } age: how long the channel has to be without activity to be deleted, count: how many channels to act on, force: whether to ignore answered status
|
|
|
|
* @memberof Modmail
|
|
|
|
*/
|
2021-10-22 09:35:04 +02:00
|
|
|
async sweepChannels ({ age, count, force = false } = {}) {
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
this.client.logger.info(`Sweeping graveyard`);
|
|
|
|
const now = Date.now();
|
2021-07-09 14:29:17 +02:00
|
|
|
if (!this.graveyard) return this.client.logger.error(`Missing graveyard category!`);
|
2021-06-20 13:11:36 +02:00
|
|
|
const graveyardChannels = this.graveyard.children.sort((a, b) => {
|
|
|
|
if (!a.lastMessage) return -1;
|
|
|
|
if (!b.lastMessage) return 1;
|
|
|
|
return a.lastMessage.createdTimestamp - b.lastMessage.createdTimestamp;
|
|
|
|
}).array();
|
|
|
|
|
|
|
|
let channelCount = 0;
|
|
|
|
if (!age) age = this.graveyardInactive * 60 * 1000;
|
|
|
|
for (const channel of graveyardChannels) {
|
|
|
|
|
|
|
|
const { lastMessage } = channel;
|
|
|
|
if (!lastMessage || now - lastMessage.createdTimestamp > age || count && channelCount <= count) {
|
|
|
|
await channel.delete().then((ch) => {
|
|
|
|
const chCache = this.cache.channels;
|
2021-10-22 09:35:04 +02:00
|
|
|
const _cached = Object.entries(chCache).find(([ , val ]) => {
|
2021-06-20 13:11:36 +02:00
|
|
|
return val === ch.id;
|
|
|
|
});
|
|
|
|
if (_cached) {
|
2021-10-22 09:35:04 +02:00
|
|
|
const [ userId ] = _cached;
|
2021-07-19 21:39:14 +02:00
|
|
|
delete this.modmail.cache.channels[userId];
|
2021-06-20 13:11:36 +02:00
|
|
|
}
|
|
|
|
}).catch((err) => {
|
|
|
|
this.client.logger.error(`Failed to delete channel from graveyard during sweep:\n${err.stack}`);
|
|
|
|
});
|
|
|
|
channelCount++;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.client.logger.info(`Swept ${channelCount} channels from graveyard, cleaning up answered...`);
|
2021-07-09 14:29:17 +02:00
|
|
|
if (!this.readMail) return this.client.logger.error(`Missing read mail category!`);
|
2021-06-20 13:11:36 +02:00
|
|
|
|
|
|
|
const answered = this.readMail.children
|
|
|
|
.filter((channel) => !channel.lastMessage || channel.lastMessage.createdTimestamp < Date.now() - this.readInactive * 60 * 1000 || force)
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (!a.lastMessage) return -1;
|
|
|
|
if (!b.lastMessage) return 1;
|
|
|
|
return a.lastMessage.createdTimestamp - b.lastMessage.createdTimestamp;
|
|
|
|
}).array();
|
|
|
|
let chCount = this.graveyard.children.size;
|
|
|
|
for (const ch of answered) {
|
|
|
|
if (chCount < 50) {
|
|
|
|
await ch.edit({ parentID: this.graveyard.id, lockPermissions: true }).catch((err) => {
|
|
|
|
this.client.logger.error(`Failed to move channel to graveyard during sweep:\n${err.stack}`);
|
|
|
|
});
|
|
|
|
chCount++;
|
|
|
|
} else break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.client.logger.info(`Sweep done. Took ${Date.now() - now}ms`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:35:04 +02:00
|
|
|
async overflow () { // Overflows new modmail category into read
|
2021-06-20 13:11:36 +02:00
|
|
|
const channels = this.newMail.children.sort((a, b) => {
|
|
|
|
if (!a.lastMessage) return -1;
|
|
|
|
if (!b.lastMessage) return 1;
|
|
|
|
return a.lastMessage.createdTimestamp - b.lastMessage.createdTimestamp;
|
|
|
|
}).array();
|
|
|
|
|
|
|
|
if (this.readMail.children.size >= 45) await this.sweepChannels({ count: 5, force: true });
|
|
|
|
|
|
|
|
let counter = 0;
|
|
|
|
for (const channel of channels) {
|
|
|
|
await channel.edit({ parentID: this.readMail.id, lockPermissions: true });
|
|
|
|
counter++;
|
|
|
|
if (counter === 5) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = ChannelHandler;
|