Initial commit

This commit is contained in:
Lily Wonhalf 2021-05-02 20:00:36 -04:00
commit 8ae81f1cc6
24 changed files with 1425 additions and 0 deletions

11
model/command-category.js Normal file
View file

@ -0,0 +1,11 @@
const CommandCategory = {
MODERATION: 'moderation',
ADMINISTRATION: 'administration',
BOT_MANAGEMENT: 'bot_management',
FUN: 'fun',
INFO: 'info',
ROLE: 'role',
RESOURCE: 'resource'
};
module.exports = CommandCategory;

View file

@ -0,0 +1,34 @@
const Config = require('../config.json');
const Guild = require('./guild');
const CommandPermission = {
/**
* @param {Message} message
* @returns {Promise.<boolean>}
*/
isMommy: async (message) => {
const member = await Guild.getMemberFromMessage(message);
return member.id === Config.mom;
},
/**
* @param {Message} message
* @returns {Promise.<boolean>}
*/
isMemberMod: async (message) => {
const member = await Guild.getMemberFromMessage(message);
return member.id === Config.mom || await Guild.isMemberMod(member);
},
/**
* @param {Message} message
* @returns {Promise.<boolean>}
*/
yes: async (message) => {
return true;
}
};
module.exports = CommandPermission;

108
model/command.js Normal file
View file

@ -0,0 +1,108 @@
const fs = require('fs');
const Discord = require('discord.js');
const Config = require('../config.json');
const Guild = require('./guild');
const cachelessRequire = (path) => {
if (typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
const Command = {
commandList: new Discord.Collection(),
commandAliases: {},
init: () => {
Command.commandList = new Discord.Collection();
Command.commandAliases = {};
fs.readdirSync('model/command/').forEach(file => {
if (file.substr(file.lastIndexOf('.')).toLowerCase() === '.js') {
const commandPath = `./command/${file}`;
const commandInstance = cachelessRequire(commandPath);
if (commandInstance !== null) {
const commandName = file.substr(0, file.lastIndexOf('.')).toLowerCase();
Command.commandList.set(commandName, commandPath);
if (commandInstance.aliases !== undefined && commandInstance.aliases !== null) {
commandInstance.aliases.forEach(alias => {
Command.commandAliases[alias.toLowerCase()] = commandName;
});
}
}
}
});
},
/**
* @param {Message} message
* @returns {boolean}
*/
parseMessage: async (message) => {
let isCommand = false;
if (message.content.toLowerCase().substr(0, Config.prefix.length) === Config.prefix) {
let content = message.content.substr(Config.prefix.length).trim().split(' ');
const calledCommand = content.shift().toLowerCase();
if (await Command.isValid(calledCommand, message)) {
const member = await Guild.getMemberFromMessage(message);
if (member === null) {
message.reply('sorry, you do not seem to be on the server.');
} else {
let commandName = calledCommand;
isCommand = true;
if (Command.commandAliases.hasOwnProperty(calledCommand)) {
commandName = Command.commandAliases[calledCommand];
}
const commandInstance = cachelessRequire(Command.commandList.get(commandName));
if (commandInstance !== null) {
commandInstance.process(message, content, Command);
} else {
Command.commandList.delete(commandName);
}
}
}
}
return isCommand;
},
/**
* @param {string} command
* @param {Message} message
* @return {Promise.<boolean>}
*/
isValid: async (command, message) => {
let canonicalCommand = command.toLowerCase();
let valid = Command.commandList.has(canonicalCommand);
if (!valid && Command.commandAliases.hasOwnProperty(canonicalCommand)) {
canonicalCommand = Command.commandAliases[command];
valid = Command.commandList.has(canonicalCommand);
}
const commandInstance = cachelessRequire(Command.commandList.get(canonicalCommand));
if (commandInstance === null) {
Command.commandList.delete(canonicalCommand);
}
valid = valid
&& commandInstance !== null
&& await commandInstance.isAllowedForContext(message);
return valid;
}
};
module.exports = Command;

56
model/command/avatar.js Normal file
View file

@ -0,0 +1,56 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Discord = require('discord.js');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
const Guild = require('../guild');
class Avatar
{
static instance = null;
constructor() {
if (Avatar.instance !== null) {
return Avatar.instance;
}
this.aliases = ['av'];
this.category = CommandCategory.FUN;
this.isAllowedForContext = CommandPermission.yes;
this.description = 'Displays the avatar of the specified member.';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
let user = null;
if (args.length > 0) {
const result = Guild.findDesignatedMemberInMessage(message);
if (result.foundMembers.length > 0) {
if (result.foundMembers[0].user !== undefined) {
user = result.foundMembers[0].user;
} else {
user = result.foundMembers[0];
}
}
} else {
user = message.author;
}
if (user !== null) {
const url = user.displayAvatarURL({ dynamic: true });
message.channel.send(new Discord.MessageAttachment(
url + '?size=2048',
user.id + url.substr(url.lastIndexOf('.'))
)).catch(error => Logger.warning(error.toString()));
} else {
message.reply('I... Have no idea who that could be, sorry.');
}
}
}
module.exports = new Avatar();

View file

@ -0,0 +1,81 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../../config.json');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
const { search } = require('../jira');
const MAX_CHARACTERS = 1950;
class Changelog
{
static instance = null;
constructor() {
if (Changelog.instance !== null) {
return Changelog.instance;
}
this.aliases = ['change-log', 'cl'];
this.category = CommandCategory.MODERATION;
this.isAllowedForContext = CommandPermission.isMemberMod;
this.description = 'Builds the changelog for a given version';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
const errorHandler = async (error) => {
if (error) {
Logger.exception(error);
}
await message.reactions.removeAll();
await message.react('❌');
}
await message.react('⏳').catch(() => {});
const issues = args.length > 0 && args[0] ? await search(args[0]).catch(errorHandler) : await search().catch(errorHandler);
const taskType = Config.jira.issueTypes.task;
const bugType = Config.jira.issueTypes.bug;
const features = issues.filter(issue => parseInt(issue.fields.issuetype.id) === taskType);
const bugs = issues.filter(issue => parseInt(issue.fields.issuetype.id) === bugType);
if (features.length < 1 && bugs.length < 1) {
await message.channel.send('No issues found.');
await message.reactions.removeAll().catch(() => {});
return;
}
const output = `${features.map(
issue => `* Feature: ${issue.key} - ${issue.fields.summary}`
).join('\n')}\n\n${bugs.map(
issue => `* Fixed: ${issue.key} - ${issue.fields.summary}`
).join('\n')}`.trim();
const messages = [];
let currentMessage = '```';
for (let line of output.split('\n')) {
if (currentMessage.length + line.length >= MAX_CHARACTERS) {
messages.push(`${currentMessage}\`\`\``);
currentMessage = '```';
}
currentMessage = `${currentMessage}\n${line}`;
}
messages.push(`${currentMessage}\`\`\``);
for (let messageToSend of messages) {
await message.channel.send(messageToSend).catch(() => {});
}
await message.reactions.removeAll().catch(() => {});
await message.react('✔').catch(() => {});
}
}
module.exports = new Changelog();

34
model/command/clean.js Normal file
View file

@ -0,0 +1,34 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../../config.json');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
class Clean
{
static instance = null;
constructor() {
if (Clean.instance !== null) {
return Clean.instance;
}
this.aliases = ['clear', 'purge'];
this.category = CommandCategory.MODERATION;
this.isAllowedForContext = CommandPermission.isMemberMod;
this.description = 'Kills the bot process';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
if (args.length > 0 && parseInt(args[0]) > 0) {
await message.channel.bulkDelete(Math.min(parseInt(args[0]) + 1, 100));
} else {
message.reply(`You have to tell me how many messages I should clean. \`${Config.prefix}clean 10\` for example.`);
}
}
}
module.exports = new Clean();

View file

@ -0,0 +1,42 @@
const { MessageEmbed } = require('discord.js');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
class CustomFront
{
static instance = null;
constructor() {
if (CustomFront.instance !== null) {
return CustomFront.instance;
}
this.aliases = ['custom-fronts', 'customfronts', 'customfront', 'cf'];
this.category = CommandCategory.FUN;
this.isAllowedForContext = CommandPermission.yes;
this.description = 'Explains what "custom front" is.';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
const embed = new MessageEmbed();
embed.setColor(APP_MAIN_COLOUR);
embed.setAuthor('Custom fronts', bot.user.displayAvatarURL({ dynamic: true }));
embed.setDescription(
'Custom fronts is a kind of status for fronts, like "blurred", "unknown member", "dissociated", etc... \n' +
'\n' +
'You don\'t want those to show up as real members in your system list but you still want to be able to ' +
'set front as one of them — that\'s where custom fronts kick in.\n' +
'\n' +
'They\'re highly customizable (as per popular request) so you can name them anything you want.'
);
return message.channel.send(embed);
}
}
module.exports = new CustomFront();

55
model/command/eval.js Normal file
View file

@ -0,0 +1,55 @@
const Discord = require('discord.js');
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../../config.json');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
// Including every model here so that it's ready to be used by the command
const Guild = require('../guild');
const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png';
class Eval
{
static instance = null;
constructor() {
if (Eval.instance !== null) {
return Eval.instance;
}
this.aliases = [];
this.category = CommandCategory.BOT_MANAGEMENT;
this.isAllowedForContext = CommandPermission.isMommy;
}
/**
* @param {Message} message
*/
async process(message) {
const code = message.content
.substr(Config.prefix.length + 'eval'.length)
.trim()
.replace(/(`{3})js\n(.+)\n\1/iu, '$2')
.trim();
await message.react('✔');
Logger.notice('Eval: ' + code);
let output = null;
try {
output = eval(`${code}`); // Spoopy! 🎃 🦇 👻 ☠ 🕷
} catch (exception) {
output = `**${exception.name}: ${exception.message}**\n${exception.stack}`;
}
const embed = new Discord.MessageEmbed()
.setAuthor('Eval', JAVASCRIPT_LOGO_URL)
.setColor(0x00FF00)
.setDescription(output);
message.channel.send(embed).catch(error => Logger.warning(error.toString()));
}
}
module.exports = new Eval();

179
model/command/help.js Normal file
View file

@ -0,0 +1,179 @@
const Discord = require('discord.js');
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../../config.json');
const EmojiCharacters = require('../../emoji-characters.json');
const Guild = require('../guild');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
const cachelessRequire = (path) => {
if (typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
class HelpDialog
{
/**
* @param {Message} message
*/
constructor(message) {
this.categoriesMapping = new Discord.Collection();
this.categoriesEmbed = new Discord.MessageEmbed();
this.originalMessage = message;
this.categoryCommandMapping = new Discord.Collection();
this.postedMessage = null;
this.usedEmojis = [];
this.stopAddingReactions = false;
}
/**
* @param {Command} Command
*/
async init(Command) {
const callableCommands = new Discord.Collection();
const commandList = Command.commandList.keyArray();
for (let i = 0; i < commandList.length; i++) {
const commandName = commandList[i];
const command = cachelessRequire(`../${Command.commandList.get(commandName)}`);
const isAllowed = await command.isAllowedForContext(this.originalMessage);
if (isAllowed) {
callableCommands.set(commandName, command);
}
}
callableCommands.forEach((command, commandName) => {
if (!this.categoryCommandMapping.has(command.category)) {
this.categoryCommandMapping.set(command.category, new Discord.Collection());
}
this.categoryCommandMapping.get(command.category).set(commandName, command);
});
let i = 1;
const categories = callableCommands.reduce((accumulator, command) => {
if (!this.categoriesMapping.array().includes(command.category)) {
let commandCategory = command.category.replace('_', ' ');
commandCategory = `${commandCategory.slice(0, 1).toUpperCase()}${commandCategory.slice(1)}`;
this.categoriesMapping.set(EmojiCharacters[i], command.category);
this.usedEmojis.push(EmojiCharacters[i]);
accumulator += `${EmojiCharacters[i]} ${commandCategory}\n`;
i++;
}
return accumulator;
}, '');
this.categoriesEmbed.setTitle('Help command');
this.categoriesEmbed.setDescription(categories);
return this.listCategories();
}
/**
* @param {Collection<Snowflake, MessageReaction>} [collection]
*/
async listCategories(collection) {
this.stopAddingReactions = true;
if (collection !== undefined && collection.size < 1) {
await this.postedMessage.reactions.removeAll();
return;
}
const member = await Guild.getMemberFromMessage(this.originalMessage);
const filter = (reaction, user) => {
const emoji = reaction.emoji.name;
return this.usedEmojis.includes(emoji) && user.id === member.user.id;
};
if (this.postedMessage === null) {
this.postedMessage = await this.originalMessage.channel.send(this.categoriesEmbed).catch(error => Logger.warning(error.toString()));
} else {
await this.postedMessage.reactions.removeAll();
await this.postedMessage.edit('', this.categoriesEmbed);
}
// 5 minutes
this.postedMessage.awaitReactions(filter, { time: 300000, max: 1 }).then(this.listCommands.bind(this)).catch(Logger.exception);
this.stopAddingReactions = false;
for (let i = 0; i < this.usedEmojis.length && !this.stopAddingReactions; i++) {
await this.postedMessage.react(this.usedEmojis[i]);
}
}
/**
* @param {Collection<Snowflake, MessageReaction>} collection
*/
async listCommands(collection) {
this.stopAddingReactions = true;
await this.postedMessage.reactions.removeAll();
if (collection.size < 1) {
this.postedMessage.edit('Timed out! You may send the command again.');
return;
}
const member = await Guild.getMemberFromMessage(this.originalMessage);
const filter = (reaction, user) => {
const emoji = reaction.emoji.name;
return emoji === '↩️' && user.id === member.user.id;
};
const category = this.categoriesMapping.get(collection.first().emoji.name);
const commandsEmbed = new Discord.MessageEmbed();
const commands = this.categoryCommandMapping.get(category).reduce((accumulator, command, commandName) => {
accumulator += `**${Config.prefix}${commandName}** ${command.description}\n\n`;
return accumulator;
}, '');
commandsEmbed.setTitle('Help command');
commandsEmbed.setDescription(commands);
this.postedMessage.edit('', commandsEmbed);
// 5 minutes
this.postedMessage.awaitReactions(filter, { time: 300000, max: 1 }).then(this.listCategories.bind(this)).catch(Logger.exception);
await this.postedMessage.react('↩️');
}
}
class Help
{
static instance = null;
constructor() {
if (Help.instance !== null) {
return Help.instance;
}
this.aliases = [];
this.category = CommandCategory.INFO;
this.isAllowedForContext = CommandPermission.yes;
this.description = 'Provides the list of commands';
}
/**
* @param {Message} message
* @param {Array} args
* @param {Command} Command
*/
async process(message, args, Command) {
const dialog = new HelpDialog(message);
return dialog.init(Command);
}
}
module.exports = new Help();

30
model/command/kill.js Normal file
View file

@ -0,0 +1,30 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../../config.json');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
class Kill
{
static instance = null;
constructor() {
if (Kill.instance !== null) {
return Kill.instance;
}
this.aliases = [];
this.category = CommandCategory.BOT_MANAGEMENT;
this.isAllowedForContext = CommandPermission.isMemberMod;
this.description = 'Kills the bot process';
}
/**
* @param {Message} message
*/
async process(message) {
await message.react('✔');
Logger.notice('killbotpls');
}
}
module.exports = new Kill();

29
model/command/reload.js Normal file
View file

@ -0,0 +1,29 @@
const Logger = require('@lilywonhalf/pretty-logger');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
class Reload
{
static instance = null;
constructor() {
if (Reload.instance !== null) {
return Reload.instance;
}
this.aliases = ['reboot'];
this.category = CommandCategory.BOT_MANAGEMENT;
this.isAllowedForContext = CommandPermission.isMemberMod;
this.description = 'Reboots the bot';
}
/**
* @param {Message} message
*/
async process(message) {
await message.reply(`OK, I'm rebooting now.`);
Logger.notice('Reboot asked');
}
}
module.exports = new Reload();

View file

@ -0,0 +1,34 @@
const Logger = require('@lilywonhalf/pretty-logger');
const CommandCategory = require('../command-category');
const CommandPermission = require('../command-permission');
class SetAvatar
{
static instance = null;
constructor() {
if (SetAvatar.instance !== null) {
return SetAvatar.instance;
}
this.aliases = ['setavatar'];
this.category = CommandCategory.BOT_MANAGEMENT;
this.isAllowedForContext = CommandPermission.isMemberMod;
this.description = 'Set the bot avatar';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
bot.user.setAvatar(args.join(' ')).then(() => {
message.reply('my avatar has been changed!')
}).catch((error) => {
message.reply('there has been an error changing my avatar. Check the logs for more details.');
Logger.exception(error);
});
}
}
module.exports = new SetAvatar();

1
model/globals.js Normal file
View file

@ -0,0 +1 @@
global.APP_MAIN_COLOUR = 'A95B44';

170
model/guild.js Normal file
View file

@ -0,0 +1,170 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const Discord = require('discord.js');
const cachelessRequire = (path) => {
if (typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
const Guild = {
/** {Guild} */
discordGuild: null,
/** {TextChannel} */
modsReviewChannel: null,
/** {TextChannel} */
speakerReviewChannel: null,
/** {TextChannel} */
officeChannel: null,
/** {Collection} */
messagesCache: new Discord.Collection(),
init: async () => {
Guild.discordGuild = bot.guilds.cache.find(guild => guild.id === Config.guild);
Guild.modsReviewChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.modsReview);
Guild.speakerReviewChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.speakerReview);
Guild.officeChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.office);
},
/**
* @param message
* @returns {Promise.<GuildMember|null>}
*/
getMemberFromMessage: async (message) => {
return await Guild.discordGuild.members.fetch(message.author).catch(exception => {
Logger.error(exception.toString());
return null;
});
},
/**
* @param {GuildMember} member
*/
isMemberMod: (member) => {
return member !== undefined && member !== null && member.roles.cache.has(Config.roles.mod);
},
/**
* @param {string} roleName
* @returns {Role|null}
*/
getRoleByName: (roleName) => {
return roleName === undefined || roleName === null ? null : Guild.discordGuild.roles.cache.find(
role => role.name.toLowerCase() === roleName.toLowerCase()
);
},
/**
* @param {GuildMember} member
* @param {Snowflake} snowflake - The Role snowflake.
* @returns {boolean}
*/
memberHasRole: (member, snowflake) => {
return member !== undefined && member !== null && member.roles.cache.some(role => role.id === snowflake);
},
/**
* @param {Message} message
* @returns {Discord.MessageEmbed}
*/
messageToEmbed: async (message) => {
const member = await Guild.getMemberFromMessage(message);
const suffix = member !== null && member.nickname !== null && member.nickname !== undefined ? ` aka ${member.nickname}` : '';
const embeds = message.embeds.filter(embed => embed.author.name && embed.author.iconURL);
let authorName = `${message.author.username}#${message.author.discriminator}${suffix}`;
let authorImage = message.author.displayAvatarURL({ dynamic: true });
let description = message.content;
let timestamp = message.createdTimestamp;
let image = null;
if (message.attachments.size > 0) {
image = message.attachments.first().url;
}
if (description.length < 1 && embeds.length > 0) {
const embed = embeds[0];
description = embed.description ? embed.description.trim() : '';
if (message.author.bot) {
if (embed.author) {
authorName = embed.author.name;
authorImage = embed.author.iconURL;
}
if (embed.timestamp) {
timestamp = embed.timestamp;
}
if (embed.image) {
image = embed.image.url;
}
}
}
return new Discord.MessageEmbed()
.setAuthor(authorName, authorImage)
.setColor(0x00FF00)
.setDescription(description)
.setTimestamp(timestamp)
.setImage(image);
},
/**
* @param {Message} message
* @returns {{certain: boolean, foundMembers: Array}}
*/
findDesignatedMemberInMessage: (message) => {
let foundMembers = [];
let certain = true;
const memberList = bot.users.cache.concat(Guild.discordGuild.members.cache);
if (message.mentions.members !== null && message.mentions.members.size > 0) {
foundMembers = message.mentions.members.array();
} else if (message.content.match(/[0-9]{18}/u) !== null) {
const ids = message.content.match(/[0-9]{18}/gu);
ids.map(id => {
if (memberList.has(id)) {
foundMembers.push(memberList.get(id));
}
});
} else {
certain = false;
memberList.forEach(member => {
const user = member.user === undefined ? member : member.user;
const hasNickname = member.nickname !== undefined && member.nickname !== null;
const nickname = hasNickname ? `${member.nickname.toLowerCase()}#${user.discriminator}` : '';
const username = `${user.username.toLowerCase()}#${user.discriminator}`;
const content = message.cleanContent.toLowerCase().split(' ').splice(1).join(' ');
if (content.length > 0) {
const contentInNickname = hasNickname ? nickname.indexOf(content) > -1 : false;
const contentInUsername = username.indexOf(content) > -1;
const nicknameInContent = hasNickname ? content.indexOf(nickname) > -1 : false;
const usernameInContent = content.indexOf(username) > -1;
if (contentInNickname || contentInUsername || nicknameInContent || usernameInContent) {
foundMembers.push(member);
}
}
});
}
return {
certain,
foundMembers
};
}
};
module.exports = Guild;

37
model/jira.js Normal file
View file

@ -0,0 +1,37 @@
const axios = require('axios');
const Config = require('../config.json');
const ENDPOINTS = {
search: '/search'
};
class Jira
{
async search(version) {
const params = {
'jql': 'project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20updated%20%3E%3D%20-24h%20ORDER%20BY%20created%20DESC',
'maxResults': 100,
'startAt': 0
};
if (version) {
params.jql = `project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20%22Fixed%20In%20Version%5BNumber%5D%22%20%3D%20%22${version}%22%20ORDER%20BY%20created%20DESC`;
}
const issues = [];
const httpParams = Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
const url = `${Config.jira.baseUrl}${ENDPOINTS.search}?${httpParams}`;
let response = null;
do {
response = await axios(url);
issues.push(...response.data.issues);
params.startAt += 100;
} while (response.data.issues.length > 99 && response.data.total > 100);
return issues;
}
}
module.exports = new Jira();

9
model/timer.js Normal file
View file

@ -0,0 +1,9 @@
/**
* @param {int} milliseconds
* @returns {Promise}
*/
global.sleep = async (milliseconds) => {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
};