From a3caa2dc42b5d76c5c8950a77dcc574edc3ac76b Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Mon, 16 Feb 2026 14:29:53 -0500 Subject: [PATCH 01/59] converted import syntax to ES modules removed unused methods --- src/bot.js | 14 +- src/commands.js | 23 +- src/db.js | 124 +-- src/enums.js | 86 +- src/{import.js => helpers/importHelper.js} | 43 +- src/helpers/memberHelper.js | 880 ++++++++++----------- src/helpers/messageHelper.js | 165 ++-- src/helpers/webhookHelper.js | 191 +++-- 8 files changed, 754 insertions(+), 772 deletions(-) rename src/{import.js => helpers/importHelper.js} (56%) diff --git a/src/bot.js b/src/bot.js index 2dd2c92..af7a319 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,9 +1,9 @@ -import { Client, Events, GatewayOpcodes } from '@fluxerjs/core'; -import { messageHelper } from "./helpers/messageHelper.js"; -import {enums} from "./enums.js"; -import {commands} from "./commands.js"; -import {webhookHelper} from "./helpers/webhookHelper.js"; -import * as env from 'dotenv'; +const {messageHelper} = require('./helpers/messageHelper.js'); +const {enums} = require('enums.js'); +const {commands} = require('commands.js'); +const {webhookHelper} = require('helpers/webhookHelper.js'); +const {Client, Events } = require('@fluxerjs/core'); +const {env} = require('dotenv'); env.config(); @@ -26,7 +26,7 @@ client.on(Events.MessageCreate, async (message) => { // If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return. if (!content.startsWith(messageHelper.prefix)) { - await webhookHelper.sendMessageAsMember(client, message, content).catch((e) => { + await webhookHelper.sendMessageAsMember(client, message).catch((e) => { throw e }); return; diff --git a/src/commands.js b/src/commands.js index 83230c7..21259a4 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,12 +1,13 @@ -import {messageHelper} from "./helpers/messageHelper.js"; -import {enums} from "./enums.js"; -import {memberHelper} from "./helpers/memberHelper.js"; -import {EmbedBuilder} from "@fluxerjs/core"; -import {importHelper} from "./import.js"; +const {messageHelper} = require('helpers/messageHelper.js') +const {enums} = require('enums.js') +const {memberHelper} = require('helpers/memberHelper.js') +const {importHelper} = require('helpers/importHelper.js'); +const {EmbedBuilder} = require('@fluxerjs/core'); -const cmds = new Map(); -cmds.set('member', { +let commands = new Map(); + +commands.set('member', { description: enums.help.SHORT_DESC_MEMBER, async execute(message, client, args) { const authorFull = `${message.author.username}#${message.author.discriminator}` @@ -23,10 +24,10 @@ cmds.set('member', { } }) -cmds.set('help', { +commands.set('help', { description: enums.help.SHORT_DESC_HELP, async execute(message) { - const fields = [...cmds.entries()].map(([name, cmd]) => ({ + const fields = [...commands.entries()].map(([name, cmd]) => ({ name: `${messageHelper.prefix}${name}`, value: cmd.description, inline: true, @@ -43,7 +44,7 @@ cmds.set('help', { }, }) -cmds.set('import', { +commands.set('import', { description: enums.help.SHORT_DESC_IMPORT, async execute(message) { if (message.content.includes('--help')) { @@ -70,4 +71,4 @@ cmds.set('import', { } }) -export const commands = cmds; \ No newline at end of file +module.exports = commands; \ No newline at end of file diff --git a/src/db.js b/src/db.js index e212051..254a3eb 100644 --- a/src/db.js +++ b/src/db.js @@ -1,5 +1,5 @@ -import {DataTypes, Sequelize} from 'sequelize'; -import * as env from 'dotenv'; +const {DataTypes, sequelize, Sequelize} = require('sequelize'); +const {env} = require('dotenv'); env.config(); @@ -10,75 +10,75 @@ if (!password) { process.exit(1); } -const database = {}; +const database = { -const sequelize = new Sequelize('postgres', 'postgres', password, { - host: 'localhost', - logging: false, - dialect: 'postgres' -}); + sequelize: new Sequelize('postgres', 'postgres', password, { + host: 'localhost', + logging: false, + dialect: 'postgres' + }), -database.sequelize = sequelize; -database.Sequelize = Sequelize; + Sequelize: Sequelize, -database.members = sequelize.define('Member', { - userid: { - type: DataTypes.STRING, - allowNull: false, - }, - name: { - type: DataTypes.STRING, - allowNull: false, - }, - displayname: { - type: DataTypes.STRING, - }, - propic: { - type: DataTypes.STRING, - }, - proxy: { - type: DataTypes.STRING, - } -}); + members: sequelize.define('Member', { + userid: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + displayname: { + type: DataTypes.STRING, + }, + propic: { + type: DataTypes.STRING, + }, + proxy: { + type: DataTypes.STRING, + } + }), -database.systems = sequelize.define('System', { - userid: { - type: DataTypes.STRING, - }, - fronter: { - type: DataTypes.STRING - }, - grouptag: { - type: DataTypes.STRING - }, - autoproxy: { - type: DataTypes.BOOLEAN, - } -}) + systems: sequelize.define('System', { + userid: { + type: DataTypes.STRING, + }, + fronter: { + type: DataTypes.STRING + }, + grouptag: { + type: DataTypes.STRING + }, + autoproxy: { + type: DataTypes.BOOLEAN, + } + }), -/** - * Checks Sequelize database connection. - */ -database.check_connection = async function() { - await sequelize.authenticate().then(async (result) => { + /** + * Checks Sequelize database connection. + */ + check_connection: async function () { + await sequelize.authenticate().then(async () => { console.log('Connection has been established successfully.'); - await syncModels(); + await this.syncModels(); }).catch(err => { console.error('Unable to connect to the database:', err); process.exit(1); }); -} + }, -/** - * Syncs Sequelize models. - */ -async function syncModels() { - await sequelize.sync().then(() => { - console.log('Models synced successfully.'); - }).catch((err) => { - console.error('Syncing models did not work', err); - process.exit(1); - }); -} + /** + * Syncs Sequelize models. + */ + async syncModels() { + await this.sequelize.sync().then(() => { + console.log('Models synced successfully.'); + }).catch((err) => { + console.error('Syncing models did not work', err); + process.exit(1); + }); + } +}; -export const db = database; \ No newline at end of file +module.exports = database; \ No newline at end of file diff --git a/src/enums.js b/src/enums.js index a9f0549..31bc58b 100644 --- a/src/enums.js +++ b/src/enums.js @@ -1,46 +1,46 @@ -const helperEnums = {}; +const enums = { + err: { + NO_MEMBER: "No such member was found.", + NO_NAME_PROVIDED: "No member name was provided for", + NO_VALUE: "has not been set for this member. Please provide a value.", + ADD_ERROR: "Error adding member.", + MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", + USER_NO_MEMBERS: "You have no members created.", + DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", + PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", + NO_SUCH_COMMAND: "No such command exists.", + PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", + PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", + NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", + NOT_IN_SERVER: "You can only proxy in a server.", + NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', + NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", + NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", + NOT_JSON_FILE: "Please attach a valid JSON file.", + NO_MEMBERS_IMPORTED: 'No members were imported.', + IMPORT_ERROR: "Please see attached file for logs on the member import process.", + }, -helperEnums.err = { - NO_MEMBER: "No such member was found.", - NO_NAME_PROVIDED: "No member name was provided for", - NO_VALUE: "has not been set for this member. Please provide a value.", - ADD_ERROR: "Error adding member.", - MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", - USER_NO_MEMBERS: "You have no members created.", - DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", - PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", - NO_SUCH_COMMAND: "No such command exists.", - PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", - PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", - NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", - NOT_IN_SERVER: "You can only proxy in a server.", - NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', - NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", - NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", - NOT_JSON_FILE: "Please attach a valid JSON file.", - NO_MEMBERS_IMPORTED: 'No members were imported.', - IMPORT_ERROR: "Please see attached file for logs on the member import process.", -} + help: { + SHORT_DESC_HELP: "Lists available commands.", + SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.", + SHORT_DESC_IMPORT: "Imports from PluralKit.", + SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", + PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", + MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", + NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", + REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", + LIST: "Lists members in the system. Currently only lists the first 25.", + NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", + DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", + PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", + PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." + }, -helperEnums.help = { - SHORT_DESC_HELP: "Lists available commands.", - SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.", - SHORT_DESC_IMPORT: "Imports from PluralKit.", - SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", - PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", - MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", - NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", - REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", - LIST: "Lists members in the system. Currently only lists the first 25.", - NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", - DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", - PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", - PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." -} + misc: { + ATTACHMENT_SENT_BY: "Attachment sent by:" + } +}; -helperEnums.misc = { - ATTACHMENT_SENT_BY: "Attachment sent by:" -} - -export const enums = helperEnums; \ No newline at end of file +module.exports = enums; \ No newline at end of file diff --git a/src/import.js b/src/helpers/importHelper.js similarity index 56% rename from src/import.js rename to src/helpers/importHelper.js index 435e7ce..1c0f8c2 100644 --- a/src/import.js +++ b/src/helpers/importHelper.js @@ -1,23 +1,22 @@ -import {enums} from "./enums.js"; -import {memberHelper} from "./helpers/memberHelper.js"; -import {messageHelper} from "./helpers/messageHelper.js"; +const {enums} = require("../enums.js"); +const {memberHelper} = require("memberHelper.js"); -const ih = {}; +const importHelper = { -/** - * Tries to import from Pluralkit. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} attachmentUrl - The attached JSON url. - * @returns {string} A successful addition of all members. - * @throws {Error} When the member exists, or creating a member doesn't work. - */ -ih.pluralKitImport = async function (authorId, attachmentUrl) { - if (!attachmentUrl) { - throw new Error(enums.err.NOT_JSON_FILE); - } - return fetch(attachmentUrl).then((res) => res.json()).then(async(pkData) => { + /** + * Tries to import from Pluralkit. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} attachmentUrl - The attached JSON url. + * @returns {string} A successful addition of all members. + * @throws {Error} When the member exists, or creating a member doesn't work. + */ + async pluralKitImport(authorId, attachmentUrl) { + if (!attachmentUrl) { + throw new Error(enums.err.NOT_JSON_FILE); + } + return fetch(attachmentUrl).then((res) => res.json()).then(async (pkData) => { const pkMembers = pkData.members; const errors = []; const addedMembers = []; @@ -29,7 +28,8 @@ ih.pluralKitImport = async function (authorId, attachmentUrl) { errors.push(`${pkMember.name}: ${e.message}`); }); await memberHelper.checkImageFormatValidity(pkMember.avatar_url).catch(e => { - errors.push(`${pkMember.name}: ${e.message}`)}); + errors.push(`${pkMember.name}: ${e.message}`) + }); } const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED; if (errors.length > 0) { @@ -37,6 +37,7 @@ ih.pluralKitImport = async function (authorId, attachmentUrl) { } return aggregatedText; }); -} + } +}; -export const importHelper = ih; \ No newline at end of file +module.exports = importHelper; \ No newline at end of file diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 0b2cf64..cf56ce3 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,466 +1,456 @@ -import {db} from '../db.js'; -import {enums} from "../enums.js"; -import {EmptyResultError, Op} from "sequelize"; -import {EmbedBuilder} from "@fluxerjs/core"; - -const mh = {}; - -// Has an empty "command" to parse the help message properly -const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', '']; - -/** - * Parses through the subcommands that come after "pf;member" and calls functions accordingly. - * - * @async - * @param {string} authorId - The id of the message author - * @param {string} authorFull - The username and discriminator of the message author - * @param {string[]} args - The message arguments - * @param {string | null} attachmentUrl - The message attachment url. - * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) - * @returns {Promise | Promise } A message, or an informational embed. - * @throws {Error} - */ -mh.parseMemberCommand = async function(authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null){ - let member; - // checks whether command is in list, otherwise assumes it's a name - if(!commandList.includes(args[0])) { - member = await mh.getMemberInfo(authorId, args[0]); - } - switch(args[0]) { - case '--help': - return enums.help.MEMBER; - case 'new': - return await mh.addNewMember(authorId, args).catch((e) =>{throw e}); - case 'remove': - return await mh.removeMember(authorId, args).catch((e) =>{throw e}); - case 'name': - return enums.help.NAME; - case 'displayname': +const { db } = require('../db.js') +const { enums} = require('../enums.js'); +const {EmptyResultError, Op} = require('sequelize'); +const {EmbedBuilder} = require('@fluxerjs/core'); + +const memberHelper = { + + + // Has an empty "command" to parse the help message properly + commandList: ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', ''], + + /** + * Parses through the subcommands that come after "pf;member" and calls functions accordingly. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorFull - The username and discriminator of the message author + * @param {string[]} args - The message arguments + * @param {string | null} attachmentUrl - The message attachment url. + * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) + * @returns {Promise | Promise } A message, or an informational embed. + * @throws {Error} + */ + async parseMemberCommand (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null){ + let member; + // checks whether command is in list, otherwise assumes it's a name + if(!this.commandList.includes(args[0])) { + member = await this.getMemberInfo(authorId, args[0]); + } + switch(args[0]) { + case '--help': + return enums.help.MEMBER; + case 'new': + return await this.addNewMember(authorId, args).catch((e) =>{throw e}); + case 'remove': + return await this.removeMember(authorId, args).catch((e) =>{throw e}); + case 'name': + return enums.help.NAME; + case 'displayname': + return enums.help.DISPLAY_NAME; + case 'proxy': + return enums.help.PROXY; + case 'propic': + return enums.help.PROPIC; + case 'list': + if (args[1] && args[1] === "--help") { + return enums.help.LIST; + } + return await this.getAllMembersInfo(authorId, authorFull).catch((e) =>{throw e}); + case '': + return enums.help.MEMBER; + } + switch(args[1]) { + case 'name': + return await this.updateName(authorId, args).catch((e) =>{throw e}); + case 'displayname': + return await this.updateDisplayName(authorId, args).catch((e) =>{throw e}); + case 'proxy': + if (!args[2]) return await this.getProxyByMember(authorId, args[0]).catch((e) => {throw e}); + return await this.updateProxy(authorId, args).catch((e) =>{throw e}); + case 'propic': + return await this.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) =>{throw e}); + default: + return member; + } + }, + + /** + * Adds a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful addition. + * @throws {Error} When the member exists, or creating a member doesn't work. + */ + async addNewMember (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.NEW; + } + const memberName = args[1]; + const displayName = args[2]; + + return await this.addFullMember(authorId, memberName, displayName).then((member) => { + let success = `Member was successfully added.\nName: ${member.dataValues.name}` + success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; + return success; + }).catch(e => { + throw e; + }) + }, + + /** + * Updates the name for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {RangeError} When the name doesn't exist. + */ + async updateName (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { return enums.help.DISPLAY_NAME; - case 'proxy': - return enums.help.PROXY; - case 'propic': - return enums.help.PROPIC; - case 'list': - if (args[1] && args[1] === "--help") { - return enums.help.LIST; - } - return await mh.getAllMembersInfo(authorId, authorFull).catch((e) =>{throw e}); - case '': - return enums.help.MEMBER; - } - switch(args[1]) { - case 'name': - return await mh.updateName(authorId, args).catch((e) =>{throw e}); - case 'displayname': - return await mh.updateDisplayName(authorId, args).catch((e) =>{throw e}); - case 'proxy': - if (!args[2]) return await mh.getProxyByMember(authorId, args[0]).catch((e) => {throw e}); - return await mh.updateProxy(authorId, args).catch((e) =>{throw e}); - case 'propic': - return await mh.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) =>{throw e}); - default: - return member; - } -} - -/** - * Adds a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful addition. - * @throws {Error} When the member exists, or creating a member doesn't work. - */ -mh.addNewMember = async function(authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.NEW; - } - const memberName = args[1]; - const displayName = args[2]; - - return await mh.addFullMember(authorId, memberName, displayName).then((member) => { - let success = `Member was successfully added.\nName: ${member.dataValues.name}` - success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; - return success; - }).catch(e => { - throw e; - }) -} - -/** - * Updates the name for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {RangeError} When the name doesn't exist. - */ -mh.updateName = async function (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.DISPLAY_NAME; - } + } - const name = args[2]; - const trimmedName = name ? name.trim() : null; - if (!name || trimmedName === null) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); - } - return await mh.updateMemberField(authorId, args).catch((e) =>{throw e}); -} + const name = args[2]; + const trimmedName = name ? name.trim() : null; + if (!name || trimmedName === null) { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + } + return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); + }, + + /** + * Updates the display name for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {RangeError} When the display name is too long or doesn't exist. + */ + async updateDisplayName (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.DISPLAY_NAME; + } -/** - * Updates the display name for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {RangeError} When the display name is too long or doesn't exist. - */ -mh.updateDisplayName = async function(authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.DISPLAY_NAME; - } + const memberName = args[0]; + const displayName = args[2]; + const trimmedName = displayName ? displayName.trim() : null; - const memberName = args[0]; - const displayName = args[2]; - const trimmedName = displayName ? displayName.trim() : null; + if (!displayName || trimmedName === null ) { + return await this.getMemberByName(authorId, memberName).then((member) => { + if (member && member.displayname) { + return `Display name for ${memberName} is: \"${member.displayname}\".`; + } + else if (member) { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + } + }); + } + else if (displayName.length > 32) { + throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); + } + return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); + }, + + /** + * Updates the proxy for a member, first checking that no other members attached to the author have the tag. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise } A successful update. + * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. + */ + async updateProxy (authorId, args) { + if (args[2] && args[2] === "--help") { + return enums.help.PROXY; + } + const proxyExists = await this.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { + return proxyExists; + }).catch((e) =>{throw e}); + if (!proxyExists) { + return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); + } + }, + + /** + * Updates the profile pic for a member, based on either the attachment or the args provided. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @param {string} attachmentUrl - The url of the first attachment in the message + * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) + * @returns {Promise} A successful update. + * @throws {Error} When loading the profile picture from a URL doesn't work. + */ + async updatePropic (authorId, args, attachmentUrl, attachmentExpiry= null) { + if (args[1] && args[1] === "--help") { + return enums.help.PROPIC; + } + let img; + const updatedArgs = args; + if (!updatedArgs[1] && !attachmentUrl) { + return enums.help.PROPIC; + } else if (attachmentUrl) { + updatedArgs[2] = attachmentUrl; + updatedArgs[3] = attachmentExpiry; + } + if (updatedArgs[2]) { + img = updatedArgs[2]; + } + const isValidImage = await this.checkImageFormatValidity(img).catch((e) =>{throw e}); + if (isValidImage) { + return await this.updateMemberField(authorId, updatedArgs).catch((e) =>{throw e}); + } + }, + + /** + * Checks if an uploaded picture is in the right format. + * + * @async + * @param {string} imageUrl - The url of the image + * @returns {Promise} - If the image is a valid format. + * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. + */ + async checkImageFormatValidity (imageUrl) { + const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; + return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { + if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); + return true; + }).catch((error) => { + throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); + }); + }, + + /** + * Removes a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful removal. + * @throws {EmptyResultError} When there is no member to remove. + */ + async removeMember (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.REMOVE; + } - if (!displayName || trimmedName === null ) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member && member.displayname) { - return `Display name for ${memberName} is: \"${member.displayname}\".`; + const memberName = args[1]; + return await db.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { + if (result) { + return `Member "${memberName}" has been deleted.`; } - else if (member) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + throw new EmptyResultError(`${enums.err.NO_MEMBER}`); + }) + }, + + /*======Non-Subcommands======*/ + + /** + * Adds a member with full details, first checking that there is no member of that name associated with the author. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} memberName - The name of the member. + * @param {string | null} displayName - The display name of the member. + * @param {string | null} proxy - The proxy tag of the member. + * @param {string | null} propic - The profile picture URL of the member. + * @param {boolean} isImport - Whether calling from the import or not. + * @returns {Promise} A successful addition. + * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. + */ + async addFullMember (authorId, memberName, displayName = null, proxy = null, propic= null, isImport = false) { + await this.getMemberByName(authorId, memberName).then((member) => { + if (member) { + throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); } }); - } - else if (displayName.length > 32) { - throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); - } - return await mh.updateMemberField(authorId, args).catch((e) =>{throw e}); -} - -/** - * Updates the proxy for a member, first checking that no other members attached to the author have the tag. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise } A successful update. - * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. - */ -mh.updateProxy = async function(authorId, args) { - if (args[2] && args[2] === "--help") { - return enums.help.PROXY; - } - const proxyExists = await mh.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { - return proxyExists; - }).catch((e) =>{throw e}); - if (!proxyExists) { - return await mh.updateMemberField(authorId, args).catch((e) =>{throw e}); - } -} - -/** - * Updates the profile pic for a member, based on either the attachment or the args provided. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @param {string} attachmentUrl - The url of the first attachment in the message - * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) - * @returns {Promise} A successful update. - * @throws {Error} When loading the profile picture from a URL doesn't work. - */ -mh.updatePropic = async function(authorId, args, attachmentUrl, attachmentExpiry= null) { - if (args[1] && args[1] === "--help") { - return enums.help.PROPIC; - } - let img; - const updatedArgs = args; - if (!updatedArgs[1] && !attachmentUrl) { - return enums.help.PROPIC; - } else if (attachmentUrl) { - updatedArgs[2] = attachmentUrl; - updatedArgs[3] = attachmentExpiry; - } - if (updatedArgs[2]) { - img = updatedArgs[2]; - } - const isValidImage = await mh.checkImageFormatValidity(img).catch((e) =>{throw e}); - if (isValidImage) { - return await mh.updateMemberField(authorId, updatedArgs).catch((e) =>{throw e}); - } -} - -/** - * Checks if an uploaded picture is in the right format. - * - * @async - * @param {string} imageUrl - The url of the image - * @returns {Promise} - If the image is a valid format. - * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. - */ -mh.checkImageFormatValidity = async function(imageUrl) { - const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; - return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { - if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); - return true; - }).catch((error) => { - throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); - }); -} - -/** - * Removes a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful removal. - * @throws {EmptyResultError} When there is no member to remove. - */ -mh.removeMember = async function(authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.REMOVE; - } - - const memberName = args[1]; - return await db.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { - if (result) { - return `Member "${memberName}" has been deleted.`; + if (displayName) { + const trimmedName = displayName ? displayName.trim() : null; + if (trimmedName && trimmedName.length > 32) { + throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); + } } - throw new EmptyResultError(`${enums.err.NO_MEMBER}`); - }) -} - -/*======Non-Subcommands======*/ - -/** - * Adds a member with full details, first checking that there is no member of that name associated with the author. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The name of the member. - * @param {string | null} displayName - The display name of the member. - * @param {string | null} proxy - The proxy tag of the member. - * @param {string | null} propic - The profile picture URL of the member. - * @param {boolean} isImport - Whether calling from the import function or not. - * @returns {Promise} A successful addition. - * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. - */ -mh.addFullMember = async function(authorId, memberName, displayName = null, proxy = null, propic= null, isImport = false) { - await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); + if (proxy) { + await this.checkIfProxyExists(authorId, proxy).catch((e) =>{throw e}); } - }); - if (displayName) { - const trimmedName = displayName ? displayName.trim() : null; - if (trimmedName && trimmedName.length > 32) { - throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); + let validPropic; + if (propic) { + validPropic = await this.checkImageFormatValidity(propic).then((valid) => { + return valid; + }).catch((e) =>{ + if (!isImport) { + throw (e); + } + return false; + }); } - } - if (proxy) { - await mh.checkIfProxyExists(authorId, proxy).catch((e) =>{throw e}); - } - let validPropic; - if (propic) { - validPropic = await mh.checkImageFormatValidity(propic).then((valid) => { - return valid; - }).catch((e) =>{ - if (!isImport) { - throw (e); - } - return false; - }); - } - const member = await db.members.create({ - name: memberName, - userid: authorId, - displayname: displayName, - proxy: proxy, - propic: validPropic ? propic : null, - }); - if (!member) { - new Error(`${enums.err.ADD_ERROR}: ${e.message}`); - } -} - -/** - * Updates one fields for a member in the database. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {EmptyResultError | Error} When the member is not found, or catchall error. - */ -mh.updateMemberField = async function(authorId, args) { - const memberName = args[0]; - const columnName = args[1]; - const value = args[2]; - let fluxerPropicWarning; - - // indicates that an attachment was uploaded on Fluxer directly - if (columnName === "propic" && args[3]) { - fluxerPropicWarning = mh.setExpirationWarning(args[3]); - } - return await db.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { - return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; - }).catch(e => { - if (e === EmptyResultError) { - throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); + const member = await db.members.create({ + name: memberName, + userid: authorId, + displayname: displayName, + proxy: proxy, + propic: validPropic ? propic : null, + }); + if (!member) { + new Error(`${enums.err.ADD_ERROR}`); } - else { - throw new Error(`Can't update ${memberName}. ${e.message}`); + }, + + /** + * Updates one fields for a member in the database. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {EmptyResultError | Error} When the member is not found, or catchall error. + */ + async updateMemberField (authorId, args) { + const memberName = args[0]; + const columnName = args[1]; + const value = args[2]; + let fluxerPropicWarning; + + // indicates that an attachment was uploaded on Fluxer directly + if (columnName === "propic" && args[3]) { + fluxerPropicWarning = this.setExpirationWarning(args[3]); } - }); -} - -/** - * Sets the warning for an expiration date. - * - * @param {string} expirationString - An expiration date string. - * @returns {string} A description of the expiration, interpolating the expiration string. - */ -mh.setExpirationWarning = function(expirationString) { - let expirationDate = new Date(expirationString); - if (!isNaN(expirationDate.valueOf())) { - expirationDate = expirationDate.toDateString(); - return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like and link to it directly` - } -} - -/** - * Gets the details for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The message arguments - * @returns {Promise} The member's info. - */ -mh.getMemberInfo = async function (authorId, memberName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return new EmbedBuilder() - .setTitle(member.name) - .setDescription(`Details for ${member.name}`) - .addFields( - {name: 'Display name: ', value: member.displayname ?? 'unset', inline: true}, - {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true}, - ) - .setImage(member.propic); + return await db.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { + return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; + }).catch(e => { + if (e === EmptyResultError) { + throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); + } + else { + throw new Error(`Can't update ${memberName}. ${e.message}`); + } + }); + }, + + /** + * Sets the warning for an expiration date. + * + * @param {string} expirationString - An expiration date string. + * @returns {string} A description of the expiration, interpolating the expiration string. + */ + setExpirationWarning(expirationString) { + let expirationDate = new Date(expirationString); + if (!isNaN(expirationDate.valueOf())) { + expirationDate = expirationDate.toDateString(); + return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like and link to it directly` } - }); -} - -/** - * Gets all members for an author. - * - * @async - * @param {string} authorId - The id of the message author - * @param {string} authorName - The id name the message author - * @returns {Promise} The info for all members. - * @throws {Error} When there are no members for an author. - */ -mh.getAllMembersInfo = async function(authorId, authorName) { - const members = await mh.getMembersByAuthor(authorId); - if (members == null) throw Error(enums.err.USER_NO_MEMBERS); - const fields = [...members.entries()].map(([name, member]) => ({ - name: member.name, - value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, - inline: true, - })); - return new EmbedBuilder() - .setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`) - .addFields(...fields); -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ -mh.getMemberByName = async function(authorId, memberName) { - return await db.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ -mh.getProxyByMember = async function(authorId, memberName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return member.dataValues.proxy; + }, + + /** + * Gets the details for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} memberName - The message arguments + * @returns {Promise} The member's info. + */ + async getMemberInfo (authorId, memberName) { + return await this.getMemberByName(authorId, memberName).then((member) => { + if (member) { + return new EmbedBuilder() + .setTitle(member.name) + .setDescription(`Details for ${member.name}`) + .addFields( + {name: 'Display name: ', value: member.displayname ?? 'unset', inline: true}, + {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true}, + ) + .setImage(member.propic); + } + }); + }, + + /** + * Gets all members for an author. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorName - The id name the message author + * @returns {Promise} The info for all members. + * @throws {Error} When there are no members for an author. + */ + async getAllMembersInfo (authorId, authorName) { + const members = await this.getMembersByAuthor(authorId); + if (members == null) throw Error(enums.err.USER_NO_MEMBERS); + const fields = [...members.entries()].map(([name, member]) => ({ + name: member.name, + value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, + inline: true, + })); + return new EmbedBuilder() + .setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`) + .addFields(...fields); + }, + + /** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message. + * @param {string} memberName - The member's name. + * @returns {Promise} The member object. + * @throws { EmptyResultError } When the member is not found. + */ + async getMemberByName (authorId, memberName) { + return await db.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); + }, + + /** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message. + * @param {string} memberName - The member's name. + * @returns {Promise} The member object. + * @throws { EmptyResultError } When the member is not found. + */ + async getProxyByMember (authorId, memberName) { + return await this.getMemberByName(authorId, memberName).then((member) => { + if (member) { + return member.dataValues.proxy; + } + throw new EmptyResultError(enums.err.NO_MEMBER); + }) + }, + + /** + * Gets all members belonging to the author. + * + * @async + * @param {string} authorId - The author of the message + * @returns {Promise} The member object array. + */ + async getMembersByAuthor (authorId) { + return await db.members.findAll({ where: { userid: authorId } }); + }, + + + /** + * Checks if proxy exists for a member. + * + * @param {string} authorId - The author of the message + * @param {string} proxy - The proxy tag. + * @returns {Promise } Whether the proxy exists. + * @throws {Error} When an empty proxy was provided, or no proxy exists. + */ + async checkIfProxyExists (authorId, proxy) { + if (proxy) { + const splitProxy = proxy.trim().split("text"); + if(splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); + if(!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); + + await this.getMembersByAuthor(authorId).then((memberList) => { + const proxyExists = memberList.some(member => member.proxy === proxy); + if (proxyExists) { + throw new Error(enums.err.PROXY_EXISTS); + } + }).catch(e =>{throw e}); } - throw new EmptyResultError(enums.err.NO_MEMBER); - }) -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} proxy - The proxy tag - * @returns {Promise} The member object. - */ -mh.getMemberByProxy = async function(authorId, proxy) { - return await db.members.findOne({ where: { userid: authorId, proxy: proxy } }); -} - -/** - * Gets all members belonging to the author. - * - * @async - * @param {string} authorId - The author of the message - * @returns {Promise} The member object array. - */ -mh.getMembersByAuthor = async function(authorId) { - return await db.members.findAll({ where: { userid: authorId } }); -} - -/** - * Checks if proxy exists for a member. - * - * @param {string} authorId - The author of the message - * @param {string} proxy - The proxy tag. - * @returns {Promise } Whether the proxy exists. - * @throws {Error} When an empty proxy was provided, or no proxy exists. - */ -mh.checkIfProxyExists = async function(authorId, proxy) { - if (proxy) { - const splitProxy = proxy.trim().split("text"); - if(splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); - if(!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); - - await mh.getMembersByAuthor(authorId).then((memberList) => { - const proxyExists = memberList.some(member => member.proxy === proxy); - if (proxyExists) { - throw new Error(enums.err.PROXY_EXISTS); - } - }).catch(e =>{throw e}); } - } -export const memberHelper = mh; \ No newline at end of file +module.exports = memberHelper; \ No newline at end of file diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index 94a200a..c297130 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -1,95 +1,96 @@ -import {memberHelper} from "./memberHelper.js"; -import {enums} from "../enums.js"; -import tmp, {setGracefulCleanup} from "tmp"; -import fs from 'fs'; -import {Message} from "@fluxerjs/core"; +const {memberHelper} = require('memberHelper.js'); +const {fs} = require('fs') +const {tmp, setGracefulCleanup } = require('tmp') +const {enums} = require('../enums.js') +const {Message} = require('@fluxerjs/core'); -const msgh = {}; +setGracefulCleanup(); -msgh.prefix = "pf;" +const messageHelper = { -setGracefulCleanup(); + prefix: "pf;", + + /** + * Parses and slices up message arguments, retaining quoted strings. + * + * @param {string} content - The full message content. + * @param {string} commandName - The command name. + * @returns {string[]} An array of arguments. + */ + parseCommandArgs(content, commandName) { + const message = content.slice(this.prefix.length + commandName.length).trim(); -/** - * Parses and slices up message arguments, retaining quoted strings. - * - * @param {string} content - The full message content. - * @param {string} commandName - The command name. - * @returns {string[]} An array of arguments. - */ -msgh.parseCommandArgs = function(content, commandName) { - const message = content.slice(msgh.prefix.length + commandName.length).trim(); + return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { + if (chara === '"') { + // checks whether string is within quotes or not + accumulator.quote ^= 1; + } else if (!accumulator.quote && chara === ' ') { + // if not currently in quoted string, push empty string to start word + accumulator.array.push(''); + } else { + // accumulates characters to the last string in the array and removes escape characters + accumulator.array[accumulator.array.length - 1] += chara.replace(/\\(.)/, "$1"); + } + return accumulator; + }, {array: ['']}).array // initial array with empty string for the reducer + }, - return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { - if (chara === '"') { - // checks whether string is within quotes or not - accumulator.quote ^= 1; - } else if (!accumulator.quote && chara === ' '){ - // if not currently in quoted string, push empty string to start word - accumulator.array.push(''); - } else { - // accumulates characters to the last string in the array and removes escape characters - accumulator.array[accumulator.array.length-1] += chara.replace(/\\(.)/,"$1"); + /** + * Parses messages to see if any part of the text matches the tags of any member belonging to an author. + * + * @param {string} authorId - The author of the message. + * @param {string} content - The full message content + * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. + * @returns {Object} The proxy message object. + * @throws {Error} If a proxy message is sent with no message within it. + */ + async parseProxyTags(authorId, content, attachmentUrl = null) { + const members = await memberHelper.getMembersByAuthor(authorId); + // If an author has no members, no sense in searching for proxy + if (members.length === 0) { + return; } - return accumulator; - }, {array: ['']}).array // initial array with empty string for the reducer -} -/** - * Parses messages to see if any part of the text matches the tags of any member belonging to an author. - * - * @param {string} authorId - The author of the message. - * @param {string} content - The full message content - * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. - * @returns {Object} The proxy message object. - * @throws {Error} If a proxy message is sent with no message within it. - */ -msgh.parseProxyTags = async function (authorId, content, attachmentUrl= null){ - const members = await memberHelper.getMembersByAuthor(authorId); - // If an author has no members, no sense in searching for proxy - if (members.length === 0) { - return; - } + const proxyMessage = {} + members.forEach(member => { + if (member.proxy) { + const splitProxy = member.proxy.split("text"); + if (content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { + proxyMessage.member = member; + if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; - const proxyMessage = {} - members.forEach(member => { - if (member.proxy) { - const splitProxy = member.proxy.split("text"); - if(content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { - proxyMessage.member = member; - if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; - - let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - escapedPrefix = new RegExp("^" + escapedPrefix); - escapedSuffix = new RegExp(escapedSuffix + "$") - proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); - if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + escapedPrefix = new RegExp("^" + escapedPrefix); + escapedSuffix = new RegExp(escapedSuffix + "$") + proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); + if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + } } - } - }) - return proxyMessage; -} + }) + return proxyMessage; + }, -/** - * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE - * - * @async - * @param {string} text - The text of the message. - * @param {Message} message - The message object. - * @throws {Error} If a proxy message is sent with no message within it. - * - */ -msgh.sendMessageAsAttachment = async function(text, message) { - if (text.length > 2000) { - tmp.file(async (err, path, fd, cleanupCallback) => { - fs.writeFile(path, text, (err) => { + /** + * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE + * + * @async + * @param {string} text - The text of the message. + * @param {Message} message - The message object. + * @throws {Error} If a proxy message is sent with no message within it. + * + */ + async sendMessageAsAttachment(text, message) { + if (text.length > 2000) { + tmp.file(async (err, path, fd, cleanupCallback) => { + fs.writeFile(path, text, (err) => { + if (err) throw err; + }) if (err) throw err; - }) - if (err) throw err; - await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); - }); + await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); + }); + } } -} +}; -export const messageHelper = msgh; +module.exports = messageHelper; diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index 5c2f0db..92158b8 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -1,112 +1,101 @@ -import {messageHelper} from "./messageHelper.js"; -import {memberHelper} from "./memberHelper.js"; -import {Webhook, Channel, Message, EmbedBuilder} from '@fluxerjs/core'; -import {enums} from "../enums.js"; - -const wh = {}; +const {messageHelper} = require('messageHelper.js'); +const {enums} = require('../enums.js'); +const {Client, Message, Webhook, Channel} = require('@fluxerjs/core'); const name = 'PluralFlux Proxy Webhook'; -/** - * Replaces a proxied message with a webhook using the member information. - * @param {Client} client - The fluxer.js client. - * @param {Message} message - The full message object. - * @throws {Error} When the proxy message is not in a server. - */ -wh.sendMessageAsMember = async function(client, message) { - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; - const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e}); - // If the message doesn't match a proxy, just return. - if (!proxyMatch.member) { - return; - } - // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs - if (!message.guildId) { - throw new Error(enums.err.NOT_IN_SERVER); - } - - if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { - return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) - } - await replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); -} +const webhookHelper = { + /** + * Replaces a proxied message with a webhook using the member information. + * @param {Client} client - The fluxer.js client. + * @param {Message} message - The full message object. + * @throws {Error} When the proxy message is not in a server. + */ + async sendMessageAsMember(client, message) { + const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; + const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e => { + throw e + }); + // If the message doesn't match a proxy, just return. + if (!proxyMatch.member) { + return; + } + // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs + if (!message.guildId) { + throw new Error(enums.err.NOT_IN_SERVER); + } -/** - * Replaces a proxied message with a webhook using the member information. - * @param {Client} client - The fluxer.js client. - * @param {Message} message - The message to be deleted. - * @param {string} text - The text to send via the webhook. - * @param {model} member - A member object from the database. - * @throws {Error} When there's no message to send. - */ -async function replaceMessage(client, message, text, member) { - if (text.length > 0 || message.attachments.size > 0) { - const channel = client.channels.get(message.channelId); - const webhook = await getOrCreateWebhook(client, channel).catch((e) =>{throw e}); - const username = member.displayname ?? member.name; - await webhook.send({content: text, username: username, avatar_url: member.propic}); - await message.delete(); - } - else { - throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); - } -} + if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { + return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) + } + await this.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e => { + throw e + }); + }, -/** - * Creates attachment embeds for the webhook since right now sending images is not supported. - * - * @param {Object[]} attachments - The attachments. - * @returns {Object[]} A series of embeds. - */ -function createAttachmentEmbedsForWebhook(attachments) { - let embeds = []; - attachments.forEach(attachment => { - const embed = new EmbedBuilder() - .setTitle(attachment.filename) - .setImage(attachment.url).toJSON() - embeds.push(embed); - }); - return embeds; -} + /** + * Replaces a proxied message with a webhook using the member information. + * @param {Client} client - The fluxer.js client. + * @param {Message} message - The message to be deleted. + * @param {string} text - The text to send via the webhook. + * @param {model} member - A member object from the database. + * @throws {Error} When there's no message to send. + */ + async replaceMessage(client, message, text, member) { + if (text.length > 0 || message.attachments.size > 0) { + const channel = client.channels.get(message.channelId); + const webhook = await this.getOrCreateWebhook(client, channel).catch((e) => { + throw e + }); + const username = member.displayname ?? member.name; + await webhook.send({content: text, username: username, avatar_url: member.propic}); + await message.delete(); + } else { + throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + } + }, + /** + * Gets or creates a webhook. + * + * @param {Client} client - The fluxer.js client. + * @param {Channel} channel - The channel the message was sent in. + * @returns {Webhook} A webhook object. + * @throws {Error} When no webhooks are allowed in the channel. + */ + async getOrCreateWebhook(client, channel) { + // If channel doesn't allow webhooks + if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); + let webhook = await this.getWebhook(client, channel).catch((e) => { + throw e + }); + if (!webhook) { + webhook = await channel.createWebhook({name: name}); + } + return webhook; + }, -/** - * Gets or creates a webhook. - * - * @param {Client} client - The fluxer.js client. - * @param {Channel} channel - The channel the message was sent in. - * @returns {Webhook} A webhook object. - * @throws {Error} When no webhooks are allowed in the channel. - */ -async function getOrCreateWebhook(client, channel) { - // If channel doesn't allow webhooks - if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); - let webhook = await getWebhook(client, channel).catch((e) =>{throw e}); - if (!webhook) { - webhook = await channel.createWebhook({name: name}); + /** + * Gets an existing webhook. + * + * @param {Client} client - The fluxer.js client. + * @param {Channel} channel - The channel the message was sent in. + * @returns {Webhook} A webhook object. + */ + async getWebhook(client, channel) { + const channelWebhooks = await channel?.fetchWebhooks() ?? []; + if (channelWebhooks.length === 0) { + return; + } + let pf_webhook; + channelWebhooks.forEach((webhook) => { + if (webhook.name === name) { + pf_webhook = webhook; + } + }) + return pf_webhook; } - return webhook; } -/** - * Gets an existing webhook. - * - * @param {Client} client - The fluxer.js client. - * @param {Channel} channel - The channel the message was sent in. - * @returns {Webhook} A webhook object. - */ -async function getWebhook(client, channel) { - const channelWebhooks = await channel?.fetchWebhooks() ?? []; - if (channelWebhooks.length === 0) { - return; - } - let pf_webhook; - channelWebhooks.forEach((webhook) => { - if (webhook.name === name) { - pf_webhook = webhook; - } - }) - return pf_webhook; -} -export const webhookHelper = wh; \ No newline at end of file +module.exports = webhookHelper; \ No newline at end of file From 5a39610547228a6aea22a2e2157fe1ac58ea8935 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Mon, 16 Feb 2026 15:10:00 -0500 Subject: [PATCH 02/59] got test sort of working (jest set up is not crashing but also not mocking correctly) --- package.json | 9 +++++ tests/helpers/membersHelper.test.js | 59 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/helpers/membersHelper.test.js diff --git a/package.json b/package.json index ced4ff3..5cf0fb8 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,14 @@ }, "scripts": { "test": "jest" + }, + "jest": { + "testEnvironment": "node", + "coveragePathIgnorePatterns": [ + "/node_modules/" + ], + "moduleFileExtensions": ["js", "json"], + "testMatch": ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"], + "verbose": true } } diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js new file mode 100644 index 0000000..bc3a4c1 --- /dev/null +++ b/tests/helpers/membersHelper.test.js @@ -0,0 +1,59 @@ +jest.mock('@fluxerjs/core', () => jest.fn()); +jest.mock('../../src/db.js', () => jest.fn()); +jest.mock('sequelize', () => jest.fn()); +jest.mock('../../src/enums.js', () => ({ + enums: jest.requireActual('../../src/enums.js') +})); + +const {enums} = require("../../src/enums.js"); +const memberHelper = require("../../src/helpers/memberHelper.js"); + +describe('parseMemberCommand', () => { + beforeAll(() => { + jest.spyOn(memberHelper, 'getMemberInfo').mockReturnValue("member info"); + jest.spyOn(memberHelper, 'addNewMember').mockReturnValue("new member"); + jest.spyOn(memberHelper, 'removeMember').mockReturnValue("remove member"); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockReturnValue("all member info"); + jest.spyOn(memberHelper, 'updateName').mockReturnValue("update name"); + jest.spyOn(memberHelper, 'updateDisplayName').mockReturnValue("update display name"); + jest.spyOn(memberHelper, 'updateProxy').mockReturnValue("update proxy"); + jest.spyOn(memberHelper, 'updatePropic').mockReturnValue("update propic"); + }); + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + + test.each([ + [['--help'], enums.help.MEMBER], + [['new'], 'add member'], + [['remove'], 'remove member'], + [['name'], enums.help.NAME], + [['displayname'], enums.help.DISPLAY_NAME], + [['proxy'], enums.help.PROXY], + [['propic'], enums.help.PROPIC], + [['list'], 'all member info'], + ['', enums.help.MEMBER], + [['jane', 'new'], enums.help.NEW], + [['somePerson'] ['name'], 'update name'], + [['somePerson', 'displayname'], 'update display name'], + [['somePerson', 'proxy'], 'update proxy'], + [['somePerson', 'propic'], 'update propic'], + [['somePerson'], 'member info'], + ])('returns correct values', async(args, expectedResult) => { + // Arrange + const authorId = '1'; + const authorFull = 'somePerson#0001'; + // Act + const result = await memberHelper.parseMemberCommand(authorId, authorFull, args); + // + expect(result).toEqual(expectedResult); + }); + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) + From 5e28cdfd0167f5df966ae51637b54ed7c782d610 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 07:11:22 -0500 Subject: [PATCH 03/59] adjusted beforeeach/beforeall so more pass --- src/helpers/memberHelper.js | 12 ++++++------ tests/helpers/membersHelper.test.js | 29 ++++++++++++++--------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index cf56ce3..693fbfc 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,4 +1,4 @@ -const { db } = require('../db.js') +const { database } = require('../db.js') const { enums} = require('../enums.js'); const {EmptyResultError, Op} = require('sequelize'); const {EmbedBuilder} = require('@fluxerjs/core'); @@ -232,7 +232,7 @@ const memberHelper = { } const memberName = args[1]; - return await db.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { + return await database.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { if (result) { return `Member "${memberName}" has been deleted.`; } @@ -282,7 +282,7 @@ const memberHelper = { }); } - const member = await db.members.create({ + const member = await database.members.create({ name: memberName, userid: authorId, displayname: displayName, @@ -313,7 +313,7 @@ const memberHelper = { if (columnName === "propic" && args[3]) { fluxerPropicWarning = this.setExpirationWarning(args[3]); } - return await db.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { + return await database.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; }).catch(e => { if (e === EmptyResultError) { @@ -394,7 +394,7 @@ const memberHelper = { * @throws { EmptyResultError } When the member is not found. */ async getMemberByName (authorId, memberName) { - return await db.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); + return await database.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); }, /** @@ -423,7 +423,7 @@ const memberHelper = { * @returns {Promise} The member object array. */ async getMembersByAuthor (authorId) { - return await db.members.findAll({ where: { userid: authorId } }); + return await database.members.findAll({ where: { userid: authorId } }); }, diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js index bc3a4c1..4b46408 100644 --- a/tests/helpers/membersHelper.test.js +++ b/tests/helpers/membersHelper.test.js @@ -9,39 +9,38 @@ const {enums} = require("../../src/enums.js"); const memberHelper = require("../../src/helpers/memberHelper.js"); describe('parseMemberCommand', () => { - beforeAll(() => { - jest.spyOn(memberHelper, 'getMemberInfo').mockReturnValue("member info"); - jest.spyOn(memberHelper, 'addNewMember').mockReturnValue("new member"); - jest.spyOn(memberHelper, 'removeMember').mockReturnValue("remove member"); - jest.spyOn(memberHelper, 'getAllMembersInfo').mockReturnValue("all member info"); - jest.spyOn(memberHelper, 'updateName').mockReturnValue("update name"); - jest.spyOn(memberHelper, 'updateDisplayName').mockReturnValue("update display name"); - jest.spyOn(memberHelper, 'updateProxy').mockReturnValue("update proxy"); - jest.spyOn(memberHelper, 'updatePropic').mockReturnValue("update propic"); - }); + beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); + jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); + jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); + jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); + jest.spyOn(memberHelper, 'updateName').mockResolvedValue("update name"); + jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); + jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); + jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); }); test.each([ [['--help'], enums.help.MEMBER], - [['new'], 'add member'], + [['new'], 'new member'], [['remove'], 'remove member'], [['name'], enums.help.NAME], [['displayname'], enums.help.DISPLAY_NAME], [['proxy'], enums.help.PROXY], [['propic'], enums.help.PROPIC], [['list'], 'all member info'], - ['', enums.help.MEMBER], - [['jane', 'new'], enums.help.NEW], - [['somePerson'] ['name'], 'update name'], + [[''], enums.help.MEMBER], + [['somePerson', 'new'], enums.help.NEW], + [['somePerson', 'name'], 'update name'], [['somePerson', 'displayname'], 'update display name'], [['somePerson', 'proxy'], 'update proxy'], [['somePerson', 'propic'], 'update propic'], [['somePerson'], 'member info'], - ])('returns correct values', async(args, expectedResult) => { + ])('%s returns correct values', async(args, expectedResult) => { // Arrange const authorId = '1'; const authorFull = 'somePerson#0001'; From 876f9486ad2cb3850ffa094e342981e12770610c Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 07:14:47 -0500 Subject: [PATCH 04/59] more correct test setup --- tests/helpers/membersHelper.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js index 4b46408..b3ee50c 100644 --- a/tests/helpers/membersHelper.test.js +++ b/tests/helpers/membersHelper.test.js @@ -21,6 +21,7 @@ describe('parseMemberCommand', () => { jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); + jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("update proxy"); }); From 5ab0d62bdb187f4d57842f0df28b654b4f0bcee6 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Mon, 16 Feb 2026 14:29:53 -0500 Subject: [PATCH 05/59] converted import syntax to commonJS removed unused methods --- src/bot.js | 14 +- src/commands.js | 23 +- src/db.js | 124 +-- src/enums.js | 86 +- src/{import.js => helpers/importHelper.js} | 43 +- src/helpers/memberHelper.js | 880 ++++++++++----------- src/helpers/messageHelper.js | 165 ++-- src/helpers/webhookHelper.js | 191 +++-- 8 files changed, 754 insertions(+), 772 deletions(-) rename src/{import.js => helpers/importHelper.js} (56%) diff --git a/src/bot.js b/src/bot.js index 2dd2c92..af7a319 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,9 +1,9 @@ -import { Client, Events, GatewayOpcodes } from '@fluxerjs/core'; -import { messageHelper } from "./helpers/messageHelper.js"; -import {enums} from "./enums.js"; -import {commands} from "./commands.js"; -import {webhookHelper} from "./helpers/webhookHelper.js"; -import * as env from 'dotenv'; +const {messageHelper} = require('./helpers/messageHelper.js'); +const {enums} = require('enums.js'); +const {commands} = require('commands.js'); +const {webhookHelper} = require('helpers/webhookHelper.js'); +const {Client, Events } = require('@fluxerjs/core'); +const {env} = require('dotenv'); env.config(); @@ -26,7 +26,7 @@ client.on(Events.MessageCreate, async (message) => { // If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return. if (!content.startsWith(messageHelper.prefix)) { - await webhookHelper.sendMessageAsMember(client, message, content).catch((e) => { + await webhookHelper.sendMessageAsMember(client, message).catch((e) => { throw e }); return; diff --git a/src/commands.js b/src/commands.js index 83230c7..21259a4 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,12 +1,13 @@ -import {messageHelper} from "./helpers/messageHelper.js"; -import {enums} from "./enums.js"; -import {memberHelper} from "./helpers/memberHelper.js"; -import {EmbedBuilder} from "@fluxerjs/core"; -import {importHelper} from "./import.js"; +const {messageHelper} = require('helpers/messageHelper.js') +const {enums} = require('enums.js') +const {memberHelper} = require('helpers/memberHelper.js') +const {importHelper} = require('helpers/importHelper.js'); +const {EmbedBuilder} = require('@fluxerjs/core'); -const cmds = new Map(); -cmds.set('member', { +let commands = new Map(); + +commands.set('member', { description: enums.help.SHORT_DESC_MEMBER, async execute(message, client, args) { const authorFull = `${message.author.username}#${message.author.discriminator}` @@ -23,10 +24,10 @@ cmds.set('member', { } }) -cmds.set('help', { +commands.set('help', { description: enums.help.SHORT_DESC_HELP, async execute(message) { - const fields = [...cmds.entries()].map(([name, cmd]) => ({ + const fields = [...commands.entries()].map(([name, cmd]) => ({ name: `${messageHelper.prefix}${name}`, value: cmd.description, inline: true, @@ -43,7 +44,7 @@ cmds.set('help', { }, }) -cmds.set('import', { +commands.set('import', { description: enums.help.SHORT_DESC_IMPORT, async execute(message) { if (message.content.includes('--help')) { @@ -70,4 +71,4 @@ cmds.set('import', { } }) -export const commands = cmds; \ No newline at end of file +module.exports = commands; \ No newline at end of file diff --git a/src/db.js b/src/db.js index e212051..254a3eb 100644 --- a/src/db.js +++ b/src/db.js @@ -1,5 +1,5 @@ -import {DataTypes, Sequelize} from 'sequelize'; -import * as env from 'dotenv'; +const {DataTypes, sequelize, Sequelize} = require('sequelize'); +const {env} = require('dotenv'); env.config(); @@ -10,75 +10,75 @@ if (!password) { process.exit(1); } -const database = {}; +const database = { -const sequelize = new Sequelize('postgres', 'postgres', password, { - host: 'localhost', - logging: false, - dialect: 'postgres' -}); + sequelize: new Sequelize('postgres', 'postgres', password, { + host: 'localhost', + logging: false, + dialect: 'postgres' + }), -database.sequelize = sequelize; -database.Sequelize = Sequelize; + Sequelize: Sequelize, -database.members = sequelize.define('Member', { - userid: { - type: DataTypes.STRING, - allowNull: false, - }, - name: { - type: DataTypes.STRING, - allowNull: false, - }, - displayname: { - type: DataTypes.STRING, - }, - propic: { - type: DataTypes.STRING, - }, - proxy: { - type: DataTypes.STRING, - } -}); + members: sequelize.define('Member', { + userid: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + displayname: { + type: DataTypes.STRING, + }, + propic: { + type: DataTypes.STRING, + }, + proxy: { + type: DataTypes.STRING, + } + }), -database.systems = sequelize.define('System', { - userid: { - type: DataTypes.STRING, - }, - fronter: { - type: DataTypes.STRING - }, - grouptag: { - type: DataTypes.STRING - }, - autoproxy: { - type: DataTypes.BOOLEAN, - } -}) + systems: sequelize.define('System', { + userid: { + type: DataTypes.STRING, + }, + fronter: { + type: DataTypes.STRING + }, + grouptag: { + type: DataTypes.STRING + }, + autoproxy: { + type: DataTypes.BOOLEAN, + } + }), -/** - * Checks Sequelize database connection. - */ -database.check_connection = async function() { - await sequelize.authenticate().then(async (result) => { + /** + * Checks Sequelize database connection. + */ + check_connection: async function () { + await sequelize.authenticate().then(async () => { console.log('Connection has been established successfully.'); - await syncModels(); + await this.syncModels(); }).catch(err => { console.error('Unable to connect to the database:', err); process.exit(1); }); -} + }, -/** - * Syncs Sequelize models. - */ -async function syncModels() { - await sequelize.sync().then(() => { - console.log('Models synced successfully.'); - }).catch((err) => { - console.error('Syncing models did not work', err); - process.exit(1); - }); -} + /** + * Syncs Sequelize models. + */ + async syncModels() { + await this.sequelize.sync().then(() => { + console.log('Models synced successfully.'); + }).catch((err) => { + console.error('Syncing models did not work', err); + process.exit(1); + }); + } +}; -export const db = database; \ No newline at end of file +module.exports = database; \ No newline at end of file diff --git a/src/enums.js b/src/enums.js index a9f0549..31bc58b 100644 --- a/src/enums.js +++ b/src/enums.js @@ -1,46 +1,46 @@ -const helperEnums = {}; +const enums = { + err: { + NO_MEMBER: "No such member was found.", + NO_NAME_PROVIDED: "No member name was provided for", + NO_VALUE: "has not been set for this member. Please provide a value.", + ADD_ERROR: "Error adding member.", + MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", + USER_NO_MEMBERS: "You have no members created.", + DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", + PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", + NO_SUCH_COMMAND: "No such command exists.", + PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", + PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", + NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", + NOT_IN_SERVER: "You can only proxy in a server.", + NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', + NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", + NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", + NOT_JSON_FILE: "Please attach a valid JSON file.", + NO_MEMBERS_IMPORTED: 'No members were imported.', + IMPORT_ERROR: "Please see attached file for logs on the member import process.", + }, -helperEnums.err = { - NO_MEMBER: "No such member was found.", - NO_NAME_PROVIDED: "No member name was provided for", - NO_VALUE: "has not been set for this member. Please provide a value.", - ADD_ERROR: "Error adding member.", - MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", - USER_NO_MEMBERS: "You have no members created.", - DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", - PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", - NO_SUCH_COMMAND: "No such command exists.", - PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", - PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", - NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", - NOT_IN_SERVER: "You can only proxy in a server.", - NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', - NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", - NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", - NOT_JSON_FILE: "Please attach a valid JSON file.", - NO_MEMBERS_IMPORTED: 'No members were imported.', - IMPORT_ERROR: "Please see attached file for logs on the member import process.", -} + help: { + SHORT_DESC_HELP: "Lists available commands.", + SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.", + SHORT_DESC_IMPORT: "Imports from PluralKit.", + SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", + PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", + MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", + NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", + REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", + LIST: "Lists members in the system. Currently only lists the first 25.", + NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", + DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", + PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", + PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." + }, -helperEnums.help = { - SHORT_DESC_HELP: "Lists available commands.", - SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.", - SHORT_DESC_IMPORT: "Imports from PluralKit.", - SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", - PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", - MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", - NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", - REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", - LIST: "Lists members in the system. Currently only lists the first 25.", - NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", - DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", - PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", - PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." -} + misc: { + ATTACHMENT_SENT_BY: "Attachment sent by:" + } +}; -helperEnums.misc = { - ATTACHMENT_SENT_BY: "Attachment sent by:" -} - -export const enums = helperEnums; \ No newline at end of file +module.exports = enums; \ No newline at end of file diff --git a/src/import.js b/src/helpers/importHelper.js similarity index 56% rename from src/import.js rename to src/helpers/importHelper.js index 435e7ce..1c0f8c2 100644 --- a/src/import.js +++ b/src/helpers/importHelper.js @@ -1,23 +1,22 @@ -import {enums} from "./enums.js"; -import {memberHelper} from "./helpers/memberHelper.js"; -import {messageHelper} from "./helpers/messageHelper.js"; +const {enums} = require("../enums.js"); +const {memberHelper} = require("memberHelper.js"); -const ih = {}; +const importHelper = { -/** - * Tries to import from Pluralkit. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} attachmentUrl - The attached JSON url. - * @returns {string} A successful addition of all members. - * @throws {Error} When the member exists, or creating a member doesn't work. - */ -ih.pluralKitImport = async function (authorId, attachmentUrl) { - if (!attachmentUrl) { - throw new Error(enums.err.NOT_JSON_FILE); - } - return fetch(attachmentUrl).then((res) => res.json()).then(async(pkData) => { + /** + * Tries to import from Pluralkit. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} attachmentUrl - The attached JSON url. + * @returns {string} A successful addition of all members. + * @throws {Error} When the member exists, or creating a member doesn't work. + */ + async pluralKitImport(authorId, attachmentUrl) { + if (!attachmentUrl) { + throw new Error(enums.err.NOT_JSON_FILE); + } + return fetch(attachmentUrl).then((res) => res.json()).then(async (pkData) => { const pkMembers = pkData.members; const errors = []; const addedMembers = []; @@ -29,7 +28,8 @@ ih.pluralKitImport = async function (authorId, attachmentUrl) { errors.push(`${pkMember.name}: ${e.message}`); }); await memberHelper.checkImageFormatValidity(pkMember.avatar_url).catch(e => { - errors.push(`${pkMember.name}: ${e.message}`)}); + errors.push(`${pkMember.name}: ${e.message}`) + }); } const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED; if (errors.length > 0) { @@ -37,6 +37,7 @@ ih.pluralKitImport = async function (authorId, attachmentUrl) { } return aggregatedText; }); -} + } +}; -export const importHelper = ih; \ No newline at end of file +module.exports = importHelper; \ No newline at end of file diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 0b2cf64..cf56ce3 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,466 +1,456 @@ -import {db} from '../db.js'; -import {enums} from "../enums.js"; -import {EmptyResultError, Op} from "sequelize"; -import {EmbedBuilder} from "@fluxerjs/core"; - -const mh = {}; - -// Has an empty "command" to parse the help message properly -const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', '']; - -/** - * Parses through the subcommands that come after "pf;member" and calls functions accordingly. - * - * @async - * @param {string} authorId - The id of the message author - * @param {string} authorFull - The username and discriminator of the message author - * @param {string[]} args - The message arguments - * @param {string | null} attachmentUrl - The message attachment url. - * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) - * @returns {Promise | Promise } A message, or an informational embed. - * @throws {Error} - */ -mh.parseMemberCommand = async function(authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null){ - let member; - // checks whether command is in list, otherwise assumes it's a name - if(!commandList.includes(args[0])) { - member = await mh.getMemberInfo(authorId, args[0]); - } - switch(args[0]) { - case '--help': - return enums.help.MEMBER; - case 'new': - return await mh.addNewMember(authorId, args).catch((e) =>{throw e}); - case 'remove': - return await mh.removeMember(authorId, args).catch((e) =>{throw e}); - case 'name': - return enums.help.NAME; - case 'displayname': +const { db } = require('../db.js') +const { enums} = require('../enums.js'); +const {EmptyResultError, Op} = require('sequelize'); +const {EmbedBuilder} = require('@fluxerjs/core'); + +const memberHelper = { + + + // Has an empty "command" to parse the help message properly + commandList: ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', ''], + + /** + * Parses through the subcommands that come after "pf;member" and calls functions accordingly. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorFull - The username and discriminator of the message author + * @param {string[]} args - The message arguments + * @param {string | null} attachmentUrl - The message attachment url. + * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) + * @returns {Promise | Promise } A message, or an informational embed. + * @throws {Error} + */ + async parseMemberCommand (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null){ + let member; + // checks whether command is in list, otherwise assumes it's a name + if(!this.commandList.includes(args[0])) { + member = await this.getMemberInfo(authorId, args[0]); + } + switch(args[0]) { + case '--help': + return enums.help.MEMBER; + case 'new': + return await this.addNewMember(authorId, args).catch((e) =>{throw e}); + case 'remove': + return await this.removeMember(authorId, args).catch((e) =>{throw e}); + case 'name': + return enums.help.NAME; + case 'displayname': + return enums.help.DISPLAY_NAME; + case 'proxy': + return enums.help.PROXY; + case 'propic': + return enums.help.PROPIC; + case 'list': + if (args[1] && args[1] === "--help") { + return enums.help.LIST; + } + return await this.getAllMembersInfo(authorId, authorFull).catch((e) =>{throw e}); + case '': + return enums.help.MEMBER; + } + switch(args[1]) { + case 'name': + return await this.updateName(authorId, args).catch((e) =>{throw e}); + case 'displayname': + return await this.updateDisplayName(authorId, args).catch((e) =>{throw e}); + case 'proxy': + if (!args[2]) return await this.getProxyByMember(authorId, args[0]).catch((e) => {throw e}); + return await this.updateProxy(authorId, args).catch((e) =>{throw e}); + case 'propic': + return await this.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) =>{throw e}); + default: + return member; + } + }, + + /** + * Adds a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful addition. + * @throws {Error} When the member exists, or creating a member doesn't work. + */ + async addNewMember (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.NEW; + } + const memberName = args[1]; + const displayName = args[2]; + + return await this.addFullMember(authorId, memberName, displayName).then((member) => { + let success = `Member was successfully added.\nName: ${member.dataValues.name}` + success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; + return success; + }).catch(e => { + throw e; + }) + }, + + /** + * Updates the name for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {RangeError} When the name doesn't exist. + */ + async updateName (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { return enums.help.DISPLAY_NAME; - case 'proxy': - return enums.help.PROXY; - case 'propic': - return enums.help.PROPIC; - case 'list': - if (args[1] && args[1] === "--help") { - return enums.help.LIST; - } - return await mh.getAllMembersInfo(authorId, authorFull).catch((e) =>{throw e}); - case '': - return enums.help.MEMBER; - } - switch(args[1]) { - case 'name': - return await mh.updateName(authorId, args).catch((e) =>{throw e}); - case 'displayname': - return await mh.updateDisplayName(authorId, args).catch((e) =>{throw e}); - case 'proxy': - if (!args[2]) return await mh.getProxyByMember(authorId, args[0]).catch((e) => {throw e}); - return await mh.updateProxy(authorId, args).catch((e) =>{throw e}); - case 'propic': - return await mh.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) =>{throw e}); - default: - return member; - } -} - -/** - * Adds a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful addition. - * @throws {Error} When the member exists, or creating a member doesn't work. - */ -mh.addNewMember = async function(authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.NEW; - } - const memberName = args[1]; - const displayName = args[2]; - - return await mh.addFullMember(authorId, memberName, displayName).then((member) => { - let success = `Member was successfully added.\nName: ${member.dataValues.name}` - success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; - return success; - }).catch(e => { - throw e; - }) -} - -/** - * Updates the name for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {RangeError} When the name doesn't exist. - */ -mh.updateName = async function (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.DISPLAY_NAME; - } + } - const name = args[2]; - const trimmedName = name ? name.trim() : null; - if (!name || trimmedName === null) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); - } - return await mh.updateMemberField(authorId, args).catch((e) =>{throw e}); -} + const name = args[2]; + const trimmedName = name ? name.trim() : null; + if (!name || trimmedName === null) { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + } + return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); + }, + + /** + * Updates the display name for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {RangeError} When the display name is too long or doesn't exist. + */ + async updateDisplayName (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.DISPLAY_NAME; + } -/** - * Updates the display name for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {RangeError} When the display name is too long or doesn't exist. - */ -mh.updateDisplayName = async function(authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.DISPLAY_NAME; - } + const memberName = args[0]; + const displayName = args[2]; + const trimmedName = displayName ? displayName.trim() : null; - const memberName = args[0]; - const displayName = args[2]; - const trimmedName = displayName ? displayName.trim() : null; + if (!displayName || trimmedName === null ) { + return await this.getMemberByName(authorId, memberName).then((member) => { + if (member && member.displayname) { + return `Display name for ${memberName} is: \"${member.displayname}\".`; + } + else if (member) { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + } + }); + } + else if (displayName.length > 32) { + throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); + } + return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); + }, + + /** + * Updates the proxy for a member, first checking that no other members attached to the author have the tag. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise } A successful update. + * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. + */ + async updateProxy (authorId, args) { + if (args[2] && args[2] === "--help") { + return enums.help.PROXY; + } + const proxyExists = await this.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { + return proxyExists; + }).catch((e) =>{throw e}); + if (!proxyExists) { + return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); + } + }, + + /** + * Updates the profile pic for a member, based on either the attachment or the args provided. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @param {string} attachmentUrl - The url of the first attachment in the message + * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) + * @returns {Promise} A successful update. + * @throws {Error} When loading the profile picture from a URL doesn't work. + */ + async updatePropic (authorId, args, attachmentUrl, attachmentExpiry= null) { + if (args[1] && args[1] === "--help") { + return enums.help.PROPIC; + } + let img; + const updatedArgs = args; + if (!updatedArgs[1] && !attachmentUrl) { + return enums.help.PROPIC; + } else if (attachmentUrl) { + updatedArgs[2] = attachmentUrl; + updatedArgs[3] = attachmentExpiry; + } + if (updatedArgs[2]) { + img = updatedArgs[2]; + } + const isValidImage = await this.checkImageFormatValidity(img).catch((e) =>{throw e}); + if (isValidImage) { + return await this.updateMemberField(authorId, updatedArgs).catch((e) =>{throw e}); + } + }, + + /** + * Checks if an uploaded picture is in the right format. + * + * @async + * @param {string} imageUrl - The url of the image + * @returns {Promise} - If the image is a valid format. + * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. + */ + async checkImageFormatValidity (imageUrl) { + const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; + return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { + if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); + return true; + }).catch((error) => { + throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); + }); + }, + + /** + * Removes a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful removal. + * @throws {EmptyResultError} When there is no member to remove. + */ + async removeMember (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.REMOVE; + } - if (!displayName || trimmedName === null ) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member && member.displayname) { - return `Display name for ${memberName} is: \"${member.displayname}\".`; + const memberName = args[1]; + return await db.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { + if (result) { + return `Member "${memberName}" has been deleted.`; } - else if (member) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + throw new EmptyResultError(`${enums.err.NO_MEMBER}`); + }) + }, + + /*======Non-Subcommands======*/ + + /** + * Adds a member with full details, first checking that there is no member of that name associated with the author. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} memberName - The name of the member. + * @param {string | null} displayName - The display name of the member. + * @param {string | null} proxy - The proxy tag of the member. + * @param {string | null} propic - The profile picture URL of the member. + * @param {boolean} isImport - Whether calling from the import or not. + * @returns {Promise} A successful addition. + * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. + */ + async addFullMember (authorId, memberName, displayName = null, proxy = null, propic= null, isImport = false) { + await this.getMemberByName(authorId, memberName).then((member) => { + if (member) { + throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); } }); - } - else if (displayName.length > 32) { - throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); - } - return await mh.updateMemberField(authorId, args).catch((e) =>{throw e}); -} - -/** - * Updates the proxy for a member, first checking that no other members attached to the author have the tag. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise } A successful update. - * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. - */ -mh.updateProxy = async function(authorId, args) { - if (args[2] && args[2] === "--help") { - return enums.help.PROXY; - } - const proxyExists = await mh.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { - return proxyExists; - }).catch((e) =>{throw e}); - if (!proxyExists) { - return await mh.updateMemberField(authorId, args).catch((e) =>{throw e}); - } -} - -/** - * Updates the profile pic for a member, based on either the attachment or the args provided. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @param {string} attachmentUrl - The url of the first attachment in the message - * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) - * @returns {Promise} A successful update. - * @throws {Error} When loading the profile picture from a URL doesn't work. - */ -mh.updatePropic = async function(authorId, args, attachmentUrl, attachmentExpiry= null) { - if (args[1] && args[1] === "--help") { - return enums.help.PROPIC; - } - let img; - const updatedArgs = args; - if (!updatedArgs[1] && !attachmentUrl) { - return enums.help.PROPIC; - } else if (attachmentUrl) { - updatedArgs[2] = attachmentUrl; - updatedArgs[3] = attachmentExpiry; - } - if (updatedArgs[2]) { - img = updatedArgs[2]; - } - const isValidImage = await mh.checkImageFormatValidity(img).catch((e) =>{throw e}); - if (isValidImage) { - return await mh.updateMemberField(authorId, updatedArgs).catch((e) =>{throw e}); - } -} - -/** - * Checks if an uploaded picture is in the right format. - * - * @async - * @param {string} imageUrl - The url of the image - * @returns {Promise} - If the image is a valid format. - * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. - */ -mh.checkImageFormatValidity = async function(imageUrl) { - const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; - return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { - if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); - return true; - }).catch((error) => { - throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); - }); -} - -/** - * Removes a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful removal. - * @throws {EmptyResultError} When there is no member to remove. - */ -mh.removeMember = async function(authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.REMOVE; - } - - const memberName = args[1]; - return await db.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { - if (result) { - return `Member "${memberName}" has been deleted.`; + if (displayName) { + const trimmedName = displayName ? displayName.trim() : null; + if (trimmedName && trimmedName.length > 32) { + throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); + } } - throw new EmptyResultError(`${enums.err.NO_MEMBER}`); - }) -} - -/*======Non-Subcommands======*/ - -/** - * Adds a member with full details, first checking that there is no member of that name associated with the author. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The name of the member. - * @param {string | null} displayName - The display name of the member. - * @param {string | null} proxy - The proxy tag of the member. - * @param {string | null} propic - The profile picture URL of the member. - * @param {boolean} isImport - Whether calling from the import function or not. - * @returns {Promise} A successful addition. - * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. - */ -mh.addFullMember = async function(authorId, memberName, displayName = null, proxy = null, propic= null, isImport = false) { - await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); + if (proxy) { + await this.checkIfProxyExists(authorId, proxy).catch((e) =>{throw e}); } - }); - if (displayName) { - const trimmedName = displayName ? displayName.trim() : null; - if (trimmedName && trimmedName.length > 32) { - throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); + let validPropic; + if (propic) { + validPropic = await this.checkImageFormatValidity(propic).then((valid) => { + return valid; + }).catch((e) =>{ + if (!isImport) { + throw (e); + } + return false; + }); } - } - if (proxy) { - await mh.checkIfProxyExists(authorId, proxy).catch((e) =>{throw e}); - } - let validPropic; - if (propic) { - validPropic = await mh.checkImageFormatValidity(propic).then((valid) => { - return valid; - }).catch((e) =>{ - if (!isImport) { - throw (e); - } - return false; - }); - } - const member = await db.members.create({ - name: memberName, - userid: authorId, - displayname: displayName, - proxy: proxy, - propic: validPropic ? propic : null, - }); - if (!member) { - new Error(`${enums.err.ADD_ERROR}: ${e.message}`); - } -} - -/** - * Updates one fields for a member in the database. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {EmptyResultError | Error} When the member is not found, or catchall error. - */ -mh.updateMemberField = async function(authorId, args) { - const memberName = args[0]; - const columnName = args[1]; - const value = args[2]; - let fluxerPropicWarning; - - // indicates that an attachment was uploaded on Fluxer directly - if (columnName === "propic" && args[3]) { - fluxerPropicWarning = mh.setExpirationWarning(args[3]); - } - return await db.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { - return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; - }).catch(e => { - if (e === EmptyResultError) { - throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); + const member = await db.members.create({ + name: memberName, + userid: authorId, + displayname: displayName, + proxy: proxy, + propic: validPropic ? propic : null, + }); + if (!member) { + new Error(`${enums.err.ADD_ERROR}`); } - else { - throw new Error(`Can't update ${memberName}. ${e.message}`); + }, + + /** + * Updates one fields for a member in the database. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {EmptyResultError | Error} When the member is not found, or catchall error. + */ + async updateMemberField (authorId, args) { + const memberName = args[0]; + const columnName = args[1]; + const value = args[2]; + let fluxerPropicWarning; + + // indicates that an attachment was uploaded on Fluxer directly + if (columnName === "propic" && args[3]) { + fluxerPropicWarning = this.setExpirationWarning(args[3]); } - }); -} - -/** - * Sets the warning for an expiration date. - * - * @param {string} expirationString - An expiration date string. - * @returns {string} A description of the expiration, interpolating the expiration string. - */ -mh.setExpirationWarning = function(expirationString) { - let expirationDate = new Date(expirationString); - if (!isNaN(expirationDate.valueOf())) { - expirationDate = expirationDate.toDateString(); - return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like and link to it directly` - } -} - -/** - * Gets the details for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The message arguments - * @returns {Promise} The member's info. - */ -mh.getMemberInfo = async function (authorId, memberName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return new EmbedBuilder() - .setTitle(member.name) - .setDescription(`Details for ${member.name}`) - .addFields( - {name: 'Display name: ', value: member.displayname ?? 'unset', inline: true}, - {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true}, - ) - .setImage(member.propic); + return await db.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { + return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; + }).catch(e => { + if (e === EmptyResultError) { + throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); + } + else { + throw new Error(`Can't update ${memberName}. ${e.message}`); + } + }); + }, + + /** + * Sets the warning for an expiration date. + * + * @param {string} expirationString - An expiration date string. + * @returns {string} A description of the expiration, interpolating the expiration string. + */ + setExpirationWarning(expirationString) { + let expirationDate = new Date(expirationString); + if (!isNaN(expirationDate.valueOf())) { + expirationDate = expirationDate.toDateString(); + return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like and link to it directly` } - }); -} - -/** - * Gets all members for an author. - * - * @async - * @param {string} authorId - The id of the message author - * @param {string} authorName - The id name the message author - * @returns {Promise} The info for all members. - * @throws {Error} When there are no members for an author. - */ -mh.getAllMembersInfo = async function(authorId, authorName) { - const members = await mh.getMembersByAuthor(authorId); - if (members == null) throw Error(enums.err.USER_NO_MEMBERS); - const fields = [...members.entries()].map(([name, member]) => ({ - name: member.name, - value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, - inline: true, - })); - return new EmbedBuilder() - .setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`) - .addFields(...fields); -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ -mh.getMemberByName = async function(authorId, memberName) { - return await db.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ -mh.getProxyByMember = async function(authorId, memberName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return member.dataValues.proxy; + }, + + /** + * Gets the details for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} memberName - The message arguments + * @returns {Promise} The member's info. + */ + async getMemberInfo (authorId, memberName) { + return await this.getMemberByName(authorId, memberName).then((member) => { + if (member) { + return new EmbedBuilder() + .setTitle(member.name) + .setDescription(`Details for ${member.name}`) + .addFields( + {name: 'Display name: ', value: member.displayname ?? 'unset', inline: true}, + {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true}, + ) + .setImage(member.propic); + } + }); + }, + + /** + * Gets all members for an author. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorName - The id name the message author + * @returns {Promise} The info for all members. + * @throws {Error} When there are no members for an author. + */ + async getAllMembersInfo (authorId, authorName) { + const members = await this.getMembersByAuthor(authorId); + if (members == null) throw Error(enums.err.USER_NO_MEMBERS); + const fields = [...members.entries()].map(([name, member]) => ({ + name: member.name, + value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, + inline: true, + })); + return new EmbedBuilder() + .setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`) + .addFields(...fields); + }, + + /** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message. + * @param {string} memberName - The member's name. + * @returns {Promise} The member object. + * @throws { EmptyResultError } When the member is not found. + */ + async getMemberByName (authorId, memberName) { + return await db.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); + }, + + /** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message. + * @param {string} memberName - The member's name. + * @returns {Promise} The member object. + * @throws { EmptyResultError } When the member is not found. + */ + async getProxyByMember (authorId, memberName) { + return await this.getMemberByName(authorId, memberName).then((member) => { + if (member) { + return member.dataValues.proxy; + } + throw new EmptyResultError(enums.err.NO_MEMBER); + }) + }, + + /** + * Gets all members belonging to the author. + * + * @async + * @param {string} authorId - The author of the message + * @returns {Promise} The member object array. + */ + async getMembersByAuthor (authorId) { + return await db.members.findAll({ where: { userid: authorId } }); + }, + + + /** + * Checks if proxy exists for a member. + * + * @param {string} authorId - The author of the message + * @param {string} proxy - The proxy tag. + * @returns {Promise } Whether the proxy exists. + * @throws {Error} When an empty proxy was provided, or no proxy exists. + */ + async checkIfProxyExists (authorId, proxy) { + if (proxy) { + const splitProxy = proxy.trim().split("text"); + if(splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); + if(!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); + + await this.getMembersByAuthor(authorId).then((memberList) => { + const proxyExists = memberList.some(member => member.proxy === proxy); + if (proxyExists) { + throw new Error(enums.err.PROXY_EXISTS); + } + }).catch(e =>{throw e}); } - throw new EmptyResultError(enums.err.NO_MEMBER); - }) -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} proxy - The proxy tag - * @returns {Promise} The member object. - */ -mh.getMemberByProxy = async function(authorId, proxy) { - return await db.members.findOne({ where: { userid: authorId, proxy: proxy } }); -} - -/** - * Gets all members belonging to the author. - * - * @async - * @param {string} authorId - The author of the message - * @returns {Promise} The member object array. - */ -mh.getMembersByAuthor = async function(authorId) { - return await db.members.findAll({ where: { userid: authorId } }); -} - -/** - * Checks if proxy exists for a member. - * - * @param {string} authorId - The author of the message - * @param {string} proxy - The proxy tag. - * @returns {Promise } Whether the proxy exists. - * @throws {Error} When an empty proxy was provided, or no proxy exists. - */ -mh.checkIfProxyExists = async function(authorId, proxy) { - if (proxy) { - const splitProxy = proxy.trim().split("text"); - if(splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); - if(!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); - - await mh.getMembersByAuthor(authorId).then((memberList) => { - const proxyExists = memberList.some(member => member.proxy === proxy); - if (proxyExists) { - throw new Error(enums.err.PROXY_EXISTS); - } - }).catch(e =>{throw e}); } - } -export const memberHelper = mh; \ No newline at end of file +module.exports = memberHelper; \ No newline at end of file diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index 94a200a..c297130 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -1,95 +1,96 @@ -import {memberHelper} from "./memberHelper.js"; -import {enums} from "../enums.js"; -import tmp, {setGracefulCleanup} from "tmp"; -import fs from 'fs'; -import {Message} from "@fluxerjs/core"; +const {memberHelper} = require('memberHelper.js'); +const {fs} = require('fs') +const {tmp, setGracefulCleanup } = require('tmp') +const {enums} = require('../enums.js') +const {Message} = require('@fluxerjs/core'); -const msgh = {}; +setGracefulCleanup(); -msgh.prefix = "pf;" +const messageHelper = { -setGracefulCleanup(); + prefix: "pf;", + + /** + * Parses and slices up message arguments, retaining quoted strings. + * + * @param {string} content - The full message content. + * @param {string} commandName - The command name. + * @returns {string[]} An array of arguments. + */ + parseCommandArgs(content, commandName) { + const message = content.slice(this.prefix.length + commandName.length).trim(); -/** - * Parses and slices up message arguments, retaining quoted strings. - * - * @param {string} content - The full message content. - * @param {string} commandName - The command name. - * @returns {string[]} An array of arguments. - */ -msgh.parseCommandArgs = function(content, commandName) { - const message = content.slice(msgh.prefix.length + commandName.length).trim(); + return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { + if (chara === '"') { + // checks whether string is within quotes or not + accumulator.quote ^= 1; + } else if (!accumulator.quote && chara === ' ') { + // if not currently in quoted string, push empty string to start word + accumulator.array.push(''); + } else { + // accumulates characters to the last string in the array and removes escape characters + accumulator.array[accumulator.array.length - 1] += chara.replace(/\\(.)/, "$1"); + } + return accumulator; + }, {array: ['']}).array // initial array with empty string for the reducer + }, - return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { - if (chara === '"') { - // checks whether string is within quotes or not - accumulator.quote ^= 1; - } else if (!accumulator.quote && chara === ' '){ - // if not currently in quoted string, push empty string to start word - accumulator.array.push(''); - } else { - // accumulates characters to the last string in the array and removes escape characters - accumulator.array[accumulator.array.length-1] += chara.replace(/\\(.)/,"$1"); + /** + * Parses messages to see if any part of the text matches the tags of any member belonging to an author. + * + * @param {string} authorId - The author of the message. + * @param {string} content - The full message content + * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. + * @returns {Object} The proxy message object. + * @throws {Error} If a proxy message is sent with no message within it. + */ + async parseProxyTags(authorId, content, attachmentUrl = null) { + const members = await memberHelper.getMembersByAuthor(authorId); + // If an author has no members, no sense in searching for proxy + if (members.length === 0) { + return; } - return accumulator; - }, {array: ['']}).array // initial array with empty string for the reducer -} -/** - * Parses messages to see if any part of the text matches the tags of any member belonging to an author. - * - * @param {string} authorId - The author of the message. - * @param {string} content - The full message content - * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. - * @returns {Object} The proxy message object. - * @throws {Error} If a proxy message is sent with no message within it. - */ -msgh.parseProxyTags = async function (authorId, content, attachmentUrl= null){ - const members = await memberHelper.getMembersByAuthor(authorId); - // If an author has no members, no sense in searching for proxy - if (members.length === 0) { - return; - } + const proxyMessage = {} + members.forEach(member => { + if (member.proxy) { + const splitProxy = member.proxy.split("text"); + if (content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { + proxyMessage.member = member; + if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; - const proxyMessage = {} - members.forEach(member => { - if (member.proxy) { - const splitProxy = member.proxy.split("text"); - if(content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { - proxyMessage.member = member; - if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; - - let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - escapedPrefix = new RegExp("^" + escapedPrefix); - escapedSuffix = new RegExp(escapedSuffix + "$") - proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); - if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + escapedPrefix = new RegExp("^" + escapedPrefix); + escapedSuffix = new RegExp(escapedSuffix + "$") + proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); + if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + } } - } - }) - return proxyMessage; -} + }) + return proxyMessage; + }, -/** - * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE - * - * @async - * @param {string} text - The text of the message. - * @param {Message} message - The message object. - * @throws {Error} If a proxy message is sent with no message within it. - * - */ -msgh.sendMessageAsAttachment = async function(text, message) { - if (text.length > 2000) { - tmp.file(async (err, path, fd, cleanupCallback) => { - fs.writeFile(path, text, (err) => { + /** + * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE + * + * @async + * @param {string} text - The text of the message. + * @param {Message} message - The message object. + * @throws {Error} If a proxy message is sent with no message within it. + * + */ + async sendMessageAsAttachment(text, message) { + if (text.length > 2000) { + tmp.file(async (err, path, fd, cleanupCallback) => { + fs.writeFile(path, text, (err) => { + if (err) throw err; + }) if (err) throw err; - }) - if (err) throw err; - await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); - }); + await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); + }); + } } -} +}; -export const messageHelper = msgh; +module.exports = messageHelper; diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index 5c2f0db..92158b8 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -1,112 +1,101 @@ -import {messageHelper} from "./messageHelper.js"; -import {memberHelper} from "./memberHelper.js"; -import {Webhook, Channel, Message, EmbedBuilder} from '@fluxerjs/core'; -import {enums} from "../enums.js"; - -const wh = {}; +const {messageHelper} = require('messageHelper.js'); +const {enums} = require('../enums.js'); +const {Client, Message, Webhook, Channel} = require('@fluxerjs/core'); const name = 'PluralFlux Proxy Webhook'; -/** - * Replaces a proxied message with a webhook using the member information. - * @param {Client} client - The fluxer.js client. - * @param {Message} message - The full message object. - * @throws {Error} When the proxy message is not in a server. - */ -wh.sendMessageAsMember = async function(client, message) { - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; - const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e}); - // If the message doesn't match a proxy, just return. - if (!proxyMatch.member) { - return; - } - // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs - if (!message.guildId) { - throw new Error(enums.err.NOT_IN_SERVER); - } - - if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { - return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) - } - await replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); -} +const webhookHelper = { + /** + * Replaces a proxied message with a webhook using the member information. + * @param {Client} client - The fluxer.js client. + * @param {Message} message - The full message object. + * @throws {Error} When the proxy message is not in a server. + */ + async sendMessageAsMember(client, message) { + const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; + const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e => { + throw e + }); + // If the message doesn't match a proxy, just return. + if (!proxyMatch.member) { + return; + } + // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs + if (!message.guildId) { + throw new Error(enums.err.NOT_IN_SERVER); + } -/** - * Replaces a proxied message with a webhook using the member information. - * @param {Client} client - The fluxer.js client. - * @param {Message} message - The message to be deleted. - * @param {string} text - The text to send via the webhook. - * @param {model} member - A member object from the database. - * @throws {Error} When there's no message to send. - */ -async function replaceMessage(client, message, text, member) { - if (text.length > 0 || message.attachments.size > 0) { - const channel = client.channels.get(message.channelId); - const webhook = await getOrCreateWebhook(client, channel).catch((e) =>{throw e}); - const username = member.displayname ?? member.name; - await webhook.send({content: text, username: username, avatar_url: member.propic}); - await message.delete(); - } - else { - throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); - } -} + if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { + return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) + } + await this.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e => { + throw e + }); + }, -/** - * Creates attachment embeds for the webhook since right now sending images is not supported. - * - * @param {Object[]} attachments - The attachments. - * @returns {Object[]} A series of embeds. - */ -function createAttachmentEmbedsForWebhook(attachments) { - let embeds = []; - attachments.forEach(attachment => { - const embed = new EmbedBuilder() - .setTitle(attachment.filename) - .setImage(attachment.url).toJSON() - embeds.push(embed); - }); - return embeds; -} + /** + * Replaces a proxied message with a webhook using the member information. + * @param {Client} client - The fluxer.js client. + * @param {Message} message - The message to be deleted. + * @param {string} text - The text to send via the webhook. + * @param {model} member - A member object from the database. + * @throws {Error} When there's no message to send. + */ + async replaceMessage(client, message, text, member) { + if (text.length > 0 || message.attachments.size > 0) { + const channel = client.channels.get(message.channelId); + const webhook = await this.getOrCreateWebhook(client, channel).catch((e) => { + throw e + }); + const username = member.displayname ?? member.name; + await webhook.send({content: text, username: username, avatar_url: member.propic}); + await message.delete(); + } else { + throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + } + }, + /** + * Gets or creates a webhook. + * + * @param {Client} client - The fluxer.js client. + * @param {Channel} channel - The channel the message was sent in. + * @returns {Webhook} A webhook object. + * @throws {Error} When no webhooks are allowed in the channel. + */ + async getOrCreateWebhook(client, channel) { + // If channel doesn't allow webhooks + if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); + let webhook = await this.getWebhook(client, channel).catch((e) => { + throw e + }); + if (!webhook) { + webhook = await channel.createWebhook({name: name}); + } + return webhook; + }, -/** - * Gets or creates a webhook. - * - * @param {Client} client - The fluxer.js client. - * @param {Channel} channel - The channel the message was sent in. - * @returns {Webhook} A webhook object. - * @throws {Error} When no webhooks are allowed in the channel. - */ -async function getOrCreateWebhook(client, channel) { - // If channel doesn't allow webhooks - if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); - let webhook = await getWebhook(client, channel).catch((e) =>{throw e}); - if (!webhook) { - webhook = await channel.createWebhook({name: name}); + /** + * Gets an existing webhook. + * + * @param {Client} client - The fluxer.js client. + * @param {Channel} channel - The channel the message was sent in. + * @returns {Webhook} A webhook object. + */ + async getWebhook(client, channel) { + const channelWebhooks = await channel?.fetchWebhooks() ?? []; + if (channelWebhooks.length === 0) { + return; + } + let pf_webhook; + channelWebhooks.forEach((webhook) => { + if (webhook.name === name) { + pf_webhook = webhook; + } + }) + return pf_webhook; } - return webhook; } -/** - * Gets an existing webhook. - * - * @param {Client} client - The fluxer.js client. - * @param {Channel} channel - The channel the message was sent in. - * @returns {Webhook} A webhook object. - */ -async function getWebhook(client, channel) { - const channelWebhooks = await channel?.fetchWebhooks() ?? []; - if (channelWebhooks.length === 0) { - return; - } - let pf_webhook; - channelWebhooks.forEach((webhook) => { - if (webhook.name === name) { - pf_webhook = webhook; - } - }) - return pf_webhook; -} -export const webhookHelper = wh; \ No newline at end of file +module.exports = webhookHelper; \ No newline at end of file From a44e2745c5cd06284dc8e792404b011933b23305 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Mon, 16 Feb 2026 15:10:00 -0500 Subject: [PATCH 06/59] got test sort of working (jest set up is not crashing but also not mocking correctly) --- package.json | 9 +++++ tests/helpers/membersHelper.test.js | 59 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/helpers/membersHelper.test.js diff --git a/package.json b/package.json index ced4ff3..5cf0fb8 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,14 @@ }, "scripts": { "test": "jest" + }, + "jest": { + "testEnvironment": "node", + "coveragePathIgnorePatterns": [ + "/node_modules/" + ], + "moduleFileExtensions": ["js", "json"], + "testMatch": ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"], + "verbose": true } } diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js new file mode 100644 index 0000000..bc3a4c1 --- /dev/null +++ b/tests/helpers/membersHelper.test.js @@ -0,0 +1,59 @@ +jest.mock('@fluxerjs/core', () => jest.fn()); +jest.mock('../../src/db.js', () => jest.fn()); +jest.mock('sequelize', () => jest.fn()); +jest.mock('../../src/enums.js', () => ({ + enums: jest.requireActual('../../src/enums.js') +})); + +const {enums} = require("../../src/enums.js"); +const memberHelper = require("../../src/helpers/memberHelper.js"); + +describe('parseMemberCommand', () => { + beforeAll(() => { + jest.spyOn(memberHelper, 'getMemberInfo').mockReturnValue("member info"); + jest.spyOn(memberHelper, 'addNewMember').mockReturnValue("new member"); + jest.spyOn(memberHelper, 'removeMember').mockReturnValue("remove member"); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockReturnValue("all member info"); + jest.spyOn(memberHelper, 'updateName').mockReturnValue("update name"); + jest.spyOn(memberHelper, 'updateDisplayName').mockReturnValue("update display name"); + jest.spyOn(memberHelper, 'updateProxy').mockReturnValue("update proxy"); + jest.spyOn(memberHelper, 'updatePropic').mockReturnValue("update propic"); + }); + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + + test.each([ + [['--help'], enums.help.MEMBER], + [['new'], 'add member'], + [['remove'], 'remove member'], + [['name'], enums.help.NAME], + [['displayname'], enums.help.DISPLAY_NAME], + [['proxy'], enums.help.PROXY], + [['propic'], enums.help.PROPIC], + [['list'], 'all member info'], + ['', enums.help.MEMBER], + [['jane', 'new'], enums.help.NEW], + [['somePerson'] ['name'], 'update name'], + [['somePerson', 'displayname'], 'update display name'], + [['somePerson', 'proxy'], 'update proxy'], + [['somePerson', 'propic'], 'update propic'], + [['somePerson'], 'member info'], + ])('returns correct values', async(args, expectedResult) => { + // Arrange + const authorId = '1'; + const authorFull = 'somePerson#0001'; + // Act + const result = await memberHelper.parseMemberCommand(authorId, authorFull, args); + // + expect(result).toEqual(expectedResult); + }); + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) + From 79d98c36181acce4093ce38cc9d24a1352917fff Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 07:11:22 -0500 Subject: [PATCH 07/59] adjusted beforeeach/beforeall so more pass --- src/helpers/memberHelper.js | 12 ++++++------ tests/helpers/membersHelper.test.js | 29 ++++++++++++++--------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index cf56ce3..693fbfc 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,4 +1,4 @@ -const { db } = require('../db.js') +const { database } = require('../db.js') const { enums} = require('../enums.js'); const {EmptyResultError, Op} = require('sequelize'); const {EmbedBuilder} = require('@fluxerjs/core'); @@ -232,7 +232,7 @@ const memberHelper = { } const memberName = args[1]; - return await db.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { + return await database.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { if (result) { return `Member "${memberName}" has been deleted.`; } @@ -282,7 +282,7 @@ const memberHelper = { }); } - const member = await db.members.create({ + const member = await database.members.create({ name: memberName, userid: authorId, displayname: displayName, @@ -313,7 +313,7 @@ const memberHelper = { if (columnName === "propic" && args[3]) { fluxerPropicWarning = this.setExpirationWarning(args[3]); } - return await db.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { + return await database.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; }).catch(e => { if (e === EmptyResultError) { @@ -394,7 +394,7 @@ const memberHelper = { * @throws { EmptyResultError } When the member is not found. */ async getMemberByName (authorId, memberName) { - return await db.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); + return await database.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); }, /** @@ -423,7 +423,7 @@ const memberHelper = { * @returns {Promise} The member object array. */ async getMembersByAuthor (authorId) { - return await db.members.findAll({ where: { userid: authorId } }); + return await database.members.findAll({ where: { userid: authorId } }); }, diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js index bc3a4c1..4b46408 100644 --- a/tests/helpers/membersHelper.test.js +++ b/tests/helpers/membersHelper.test.js @@ -9,39 +9,38 @@ const {enums} = require("../../src/enums.js"); const memberHelper = require("../../src/helpers/memberHelper.js"); describe('parseMemberCommand', () => { - beforeAll(() => { - jest.spyOn(memberHelper, 'getMemberInfo').mockReturnValue("member info"); - jest.spyOn(memberHelper, 'addNewMember').mockReturnValue("new member"); - jest.spyOn(memberHelper, 'removeMember').mockReturnValue("remove member"); - jest.spyOn(memberHelper, 'getAllMembersInfo').mockReturnValue("all member info"); - jest.spyOn(memberHelper, 'updateName').mockReturnValue("update name"); - jest.spyOn(memberHelper, 'updateDisplayName').mockReturnValue("update display name"); - jest.spyOn(memberHelper, 'updateProxy').mockReturnValue("update proxy"); - jest.spyOn(memberHelper, 'updatePropic').mockReturnValue("update propic"); - }); + beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); + jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); + jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); + jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); + jest.spyOn(memberHelper, 'updateName').mockResolvedValue("update name"); + jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); + jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); + jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); }); test.each([ [['--help'], enums.help.MEMBER], - [['new'], 'add member'], + [['new'], 'new member'], [['remove'], 'remove member'], [['name'], enums.help.NAME], [['displayname'], enums.help.DISPLAY_NAME], [['proxy'], enums.help.PROXY], [['propic'], enums.help.PROPIC], [['list'], 'all member info'], - ['', enums.help.MEMBER], - [['jane', 'new'], enums.help.NEW], - [['somePerson'] ['name'], 'update name'], + [[''], enums.help.MEMBER], + [['somePerson', 'new'], enums.help.NEW], + [['somePerson', 'name'], 'update name'], [['somePerson', 'displayname'], 'update display name'], [['somePerson', 'proxy'], 'update proxy'], [['somePerson', 'propic'], 'update propic'], [['somePerson'], 'member info'], - ])('returns correct values', async(args, expectedResult) => { + ])('%s returns correct values', async(args, expectedResult) => { // Arrange const authorId = '1'; const authorFull = 'somePerson#0001'; From 0a4bfa59adc6c5f19f23b720f6254f3bbd0b82d1 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 07:14:47 -0500 Subject: [PATCH 08/59] more correct test setup --- tests/helpers/membersHelper.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js index 4b46408..b3ee50c 100644 --- a/tests/helpers/membersHelper.test.js +++ b/tests/helpers/membersHelper.test.js @@ -21,6 +21,7 @@ describe('parseMemberCommand', () => { jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); + jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("update proxy"); }); From 321fe7f0a911fa83a958a9c25140a5f0cde967ab Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 17:16:48 -0500 Subject: [PATCH 09/59] more correct dockerfile and compose.yaml --- Dockerfile | 10 ++++++++++ compose.yaml | 38 ++++++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..08de148 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev + +FROM node:20-alpine +WORKDIR /app +COPY --from=builder /app/node_modules ./node_modules +COPY . . +CMD ["node", "src/bot.js"] \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 8df9cb1..eccef16 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,6 +2,9 @@ services: main: build: . container_name: pluralflux + restart: unless-stopped + networks: + - pluralflux-net postgres: image: postgres:latest container_name: pluralflux-postgres @@ -13,20 +16,27 @@ services: - pgdata:/var/lib/postgresql ports: - "5432:5432" -# pgadmin: -# image: dpage/pgadmin4:latest -# ports: -# - 5050:80 -# environment: -# # Required by pgAdmin -# PGADMIN_DEFAULT_EMAIL: pieartsy@pm.me -# PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/postgres_pwd -# # Don't require the user to login -# PGADMIN_CONFIG_SERVER_MODE: 'False' -# # Don't require a "master" password after logging in -# PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' -# secrets: -# - postgres_pwd + networks: + - pluralflux-net + pgadmin: + image: dpage/pgadmin4:latest + container_name: pluralflux-pgadmin + ports: + - "5050:80" + environment: + PGADMIN_DEFAULT_EMAIL: code@asterfialla.com + PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/postgres_pwd + PGADMIN_CONFIG_SERVER_MODE: 'False' + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' + secrets: + - postgres_pwd + depends_on: + - postgres + networks: + - pluralflux-net + +networks: + pluralflux-net: volumes: pgdata: From 35b454bc80243780b3187f9b4f70881daa45176f Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 17:25:18 -0500 Subject: [PATCH 10/59] Revert "converted import syntax to commonJS" This reverts commit 5ab0d62b --- src/bot.js | 14 +- src/commands.js | 23 +- src/db.js | 122 +-- src/enums.js | 86 +- src/helpers/memberHelper.js | 912 +++++++++++---------- src/helpers/messageHelper.js | 165 ++-- src/helpers/webhookHelper.js | 174 ++-- src/{helpers/importHelper.js => import.js} | 42 +- 8 files changed, 785 insertions(+), 753 deletions(-) rename src/{helpers/importHelper.js => import.js} (56%) diff --git a/src/bot.js b/src/bot.js index af7a319..3e9d62d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,9 +1,9 @@ -const {messageHelper} = require('./helpers/messageHelper.js'); -const {enums} = require('enums.js'); -const {commands} = require('commands.js'); -const {webhookHelper} = require('helpers/webhookHelper.js'); -const {Client, Events } = require('@fluxerjs/core'); -const {env} = require('dotenv'); +import { Client, Events } from '@fluxerjs/core'; +import { messageHelper } from "./helpers/messageHelper.js"; +import {enums} from "./enums.js"; +import {commands} from "./commands.js"; +import {webhookHelper} from "./helpers/webhookHelper.js"; +import * as env from 'dotenv'; env.config(); @@ -26,7 +26,7 @@ client.on(Events.MessageCreate, async (message) => { // If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return. if (!content.startsWith(messageHelper.prefix)) { - await webhookHelper.sendMessageAsMember(client, message).catch((e) => { + await webhookHelper.sendMessageAsMember(client, message, content).catch((e) => { throw e }); return; diff --git a/src/commands.js b/src/commands.js index 21259a4..83230c7 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,13 +1,12 @@ -const {messageHelper} = require('helpers/messageHelper.js') -const {enums} = require('enums.js') -const {memberHelper} = require('helpers/memberHelper.js') -const {importHelper} = require('helpers/importHelper.js'); -const {EmbedBuilder} = require('@fluxerjs/core'); +import {messageHelper} from "./helpers/messageHelper.js"; +import {enums} from "./enums.js"; +import {memberHelper} from "./helpers/memberHelper.js"; +import {EmbedBuilder} from "@fluxerjs/core"; +import {importHelper} from "./import.js"; +const cmds = new Map(); -let commands = new Map(); - -commands.set('member', { +cmds.set('member', { description: enums.help.SHORT_DESC_MEMBER, async execute(message, client, args) { const authorFull = `${message.author.username}#${message.author.discriminator}` @@ -24,10 +23,10 @@ commands.set('member', { } }) -commands.set('help', { +cmds.set('help', { description: enums.help.SHORT_DESC_HELP, async execute(message) { - const fields = [...commands.entries()].map(([name, cmd]) => ({ + const fields = [...cmds.entries()].map(([name, cmd]) => ({ name: `${messageHelper.prefix}${name}`, value: cmd.description, inline: true, @@ -44,7 +43,7 @@ commands.set('help', { }, }) -commands.set('import', { +cmds.set('import', { description: enums.help.SHORT_DESC_IMPORT, async execute(message) { if (message.content.includes('--help')) { @@ -71,4 +70,4 @@ commands.set('import', { } }) -module.exports = commands; \ No newline at end of file +export const commands = cmds; \ No newline at end of file diff --git a/src/db.js b/src/db.js index 254a3eb..6d8702d 100644 --- a/src/db.js +++ b/src/db.js @@ -1,5 +1,5 @@ -const {DataTypes, sequelize, Sequelize} = require('sequelize'); -const {env} = require('dotenv'); +import {DataTypes, Sequelize} from 'sequelize'; +import * as env from 'dotenv'; env.config(); @@ -10,75 +10,75 @@ if (!password) { process.exit(1); } -const database = { +const db = {}; - sequelize: new Sequelize('postgres', 'postgres', password, { - host: 'localhost', - logging: false, - dialect: 'postgres' - }), +const sequelize = new Sequelize('postgres', 'postgres', password, { + host: 'localhost', + logging: false, + dialect: 'postgres' +}); - Sequelize: Sequelize, +db.sequelize = sequelize; +db.Sequelize = Sequelize; - members: sequelize.define('Member', { - userid: { - type: DataTypes.STRING, - allowNull: false, - }, - name: { - type: DataTypes.STRING, - allowNull: false, - }, - displayname: { - type: DataTypes.STRING, - }, - propic: { - type: DataTypes.STRING, - }, - proxy: { - type: DataTypes.STRING, - } - }), +db.members = sequelize.define('Member', { + userid: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + displayname: { + type: DataTypes.STRING, + }, + propic: { + type: DataTypes.STRING, + }, + proxy: { + type: DataTypes.STRING, + } +}); - systems: sequelize.define('System', { - userid: { - type: DataTypes.STRING, - }, - fronter: { - type: DataTypes.STRING - }, - grouptag: { - type: DataTypes.STRING - }, - autoproxy: { - type: DataTypes.BOOLEAN, - } - }), +db.systems = sequelize.define('System', { + userid: { + type: DataTypes.STRING, + }, + fronter: { + type: DataTypes.STRING + }, + grouptag: { + type: DataTypes.STRING + }, + autoproxy: { + type: DataTypes.BOOLEAN, + } +}) - /** - * Checks Sequelize database connection. - */ - check_connection: async function () { +/** + * Checks Sequelize database connection. + */ +db.check_connection = async function() { await sequelize.authenticate().then(async () => { console.log('Connection has been established successfully.'); - await this.syncModels(); + await syncModels(); }).catch(err => { console.error('Unable to connect to the database:', err); process.exit(1); }); - }, +} - /** - * Syncs Sequelize models. - */ - async syncModels() { - await this.sequelize.sync().then(() => { - console.log('Models synced successfully.'); - }).catch((err) => { - console.error('Syncing models did not work', err); - process.exit(1); - }); - } -}; +/** + * Syncs Sequelize models. + */ +async function syncModels() { + await sequelize.sync().then(() => { + console.log('Models synced successfully.'); + }).catch((err) => { + console.error('Syncing models did not work', err); + process.exit(1); + }); +} -module.exports = database; \ No newline at end of file +export const database = db; \ No newline at end of file diff --git a/src/enums.js b/src/enums.js index 31bc58b..a9f0549 100644 --- a/src/enums.js +++ b/src/enums.js @@ -1,46 +1,46 @@ -const enums = { - err: { - NO_MEMBER: "No such member was found.", - NO_NAME_PROVIDED: "No member name was provided for", - NO_VALUE: "has not been set for this member. Please provide a value.", - ADD_ERROR: "Error adding member.", - MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", - USER_NO_MEMBERS: "You have no members created.", - DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", - PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", - NO_SUCH_COMMAND: "No such command exists.", - PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", - PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", - NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", - NOT_IN_SERVER: "You can only proxy in a server.", - NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', - NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", - NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", - NOT_JSON_FILE: "Please attach a valid JSON file.", - NO_MEMBERS_IMPORTED: 'No members were imported.', - IMPORT_ERROR: "Please see attached file for logs on the member import process.", - }, +const helperEnums = {}; - help: { - SHORT_DESC_HELP: "Lists available commands.", - SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.", - SHORT_DESC_IMPORT: "Imports from PluralKit.", - SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", - PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", - MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", - NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", - REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", - LIST: "Lists members in the system. Currently only lists the first 25.", - NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", - DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", - PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", - PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." - }, +helperEnums.err = { + NO_MEMBER: "No such member was found.", + NO_NAME_PROVIDED: "No member name was provided for", + NO_VALUE: "has not been set for this member. Please provide a value.", + ADD_ERROR: "Error adding member.", + MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", + USER_NO_MEMBERS: "You have no members created.", + DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", + PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", + NO_SUCH_COMMAND: "No such command exists.", + PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", + PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", + NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", + NOT_IN_SERVER: "You can only proxy in a server.", + NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', + NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", + NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", + NOT_JSON_FILE: "Please attach a valid JSON file.", + NO_MEMBERS_IMPORTED: 'No members were imported.', + IMPORT_ERROR: "Please see attached file for logs on the member import process.", +} - misc: { - ATTACHMENT_SENT_BY: "Attachment sent by:" - } -}; +helperEnums.help = { + SHORT_DESC_HELP: "Lists available commands.", + SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.", + SHORT_DESC_IMPORT: "Imports from PluralKit.", + SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", + PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", + MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", + NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", + REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", + LIST: "Lists members in the system. Currently only lists the first 25.", + NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", + DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", + PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", + PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." +} -module.exports = enums; \ No newline at end of file +helperEnums.misc = { + ATTACHMENT_SENT_BY: "Attachment sent by:" +} + +export const enums = helperEnums; \ No newline at end of file diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 693fbfc..3bee3cb 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,456 +1,500 @@ -const { database } = require('../db.js') -const { enums} = require('../enums.js'); -const {EmptyResultError, Op} = require('sequelize'); -const {EmbedBuilder} = require('@fluxerjs/core'); - -const memberHelper = { - - - // Has an empty "command" to parse the help message properly - commandList: ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', ''], - - /** - * Parses through the subcommands that come after "pf;member" and calls functions accordingly. - * - * @async - * @param {string} authorId - The id of the message author - * @param {string} authorFull - The username and discriminator of the message author - * @param {string[]} args - The message arguments - * @param {string | null} attachmentUrl - The message attachment url. - * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) - * @returns {Promise | Promise } A message, or an informational embed. - * @throws {Error} - */ - async parseMemberCommand (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null){ - let member; - // checks whether command is in list, otherwise assumes it's a name - if(!this.commandList.includes(args[0])) { - member = await this.getMemberInfo(authorId, args[0]); - } - switch(args[0]) { - case '--help': - return enums.help.MEMBER; - case 'new': - return await this.addNewMember(authorId, args).catch((e) =>{throw e}); - case 'remove': - return await this.removeMember(authorId, args).catch((e) =>{throw e}); - case 'name': - return enums.help.NAME; - case 'displayname': - return enums.help.DISPLAY_NAME; - case 'proxy': - return enums.help.PROXY; - case 'propic': - return enums.help.PROPIC; - case 'list': - if (args[1] && args[1] === "--help") { - return enums.help.LIST; - } - return await this.getAllMembersInfo(authorId, authorFull).catch((e) =>{throw e}); - case '': - return enums.help.MEMBER; - } - switch(args[1]) { - case 'name': - return await this.updateName(authorId, args).catch((e) =>{throw e}); - case 'displayname': - return await this.updateDisplayName(authorId, args).catch((e) =>{throw e}); - case 'proxy': - if (!args[2]) return await this.getProxyByMember(authorId, args[0]).catch((e) => {throw e}); - return await this.updateProxy(authorId, args).catch((e) =>{throw e}); - case 'propic': - return await this.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) =>{throw e}); - default: - return member; - } - }, - - /** - * Adds a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful addition. - * @throws {Error} When the member exists, or creating a member doesn't work. - */ - async addNewMember (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.NEW; - } - const memberName = args[1]; - const displayName = args[2]; - - return await this.addFullMember(authorId, memberName, displayName).then((member) => { - let success = `Member was successfully added.\nName: ${member.dataValues.name}` - success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; - return success; - }).catch(e => { - throw e; - }) - }, - - /** - * Updates the name for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {RangeError} When the name doesn't exist. - */ - async updateName (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { +import {database} from '../db.js'; +import {enums} from "../enums.js"; +import {EmptyResultError, Op} from "sequelize"; +import {EmbedBuilder} from "@fluxerjs/core"; + +const mh = {}; + +// Has an empty "command" to parse the help message properly +const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', '']; + +/** + * Parses through the subcommands that come after "pf;member" and calls functions accordingly. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorFull - The username and discriminator of the message author + * @param {string[]} args - The message arguments + * @param {string | null} attachmentUrl - The message attachment url. + * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) + * @returns {Promise | Promise } A message, or an informational embed. + * @throws {Error} + */ +mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) { + let member; + // checks whether command is in list, otherwise assumes it's a name + if (!commandList.includes(args[0])) { + member = await mh.getMemberInfo(authorId, args[0]); + } + switch (args[0]) { + case '--help': + return enums.help.MEMBER; + case 'new': + return await mh.addNewMember(authorId, args).catch((e) => { + throw e + }); + case 'remove': + return await mh.removeMember(authorId, args).catch((e) => { + throw e + }); + case 'name': + return enums.help.NAME; + case 'displayname': return enums.help.DISPLAY_NAME; - } + case 'proxy': + return enums.help.PROXY; + case 'propic': + return enums.help.PROPIC; + case 'list': + if (args[1] && args[1] === "--help") { + return enums.help.LIST; + } + return await mh.getAllMembersInfo(authorId, authorFull).catch((e) => { + throw e + }); + case '': + return enums.help.MEMBER; + } + switch (args[1]) { + case 'name': + return await mh.updateName(authorId, args).catch((e) => { + throw e + }); + case 'displayname': + return await mh.updateDisplayName(authorId, args).catch((e) => { + throw e + }); + case 'proxy': + if (!args[2]) return await mh.getProxyByMember(authorId, args[0]).catch((e) => { + throw e + }); + return await mh.updateProxy(authorId, args).catch((e) => { + throw e + }); + case 'propic': + return await mh.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) => { + throw e + }); + default: + return member; + } +} - const name = args[2]; - const trimmedName = name ? name.trim() : null; - if (!name || trimmedName === null) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); - } - return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); - }, - - /** - * Updates the display name for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {RangeError} When the display name is too long or doesn't exist. - */ - async updateDisplayName (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.DISPLAY_NAME; - } +/** + * Adds a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful addition. + * @throws {Error} When the member exists, or creating a member doesn't work. + */ +mh.addNewMember = async function (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.NEW; + } + const memberName = args[1]; + const displayName = args[2]; + + return await mh.addFullMember(authorId, memberName, displayName).then((member) => { + let success = `Member was successfully added.\nName: ${member.dataValues.name}` + success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; + return success; + }).catch(e => { + throw e; + }) +} - const memberName = args[0]; - const displayName = args[2]; - const trimmedName = displayName ? displayName.trim() : null; +/** + * Updates the name for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {RangeError} When the name doesn't exist. + */ +mh.updateName = async function (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.DISPLAY_NAME; + } - if (!displayName || trimmedName === null ) { - return await this.getMemberByName(authorId, memberName).then((member) => { - if (member && member.displayname) { - return `Display name for ${memberName} is: \"${member.displayname}\".`; - } - else if (member) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); - } - }); - } - else if (displayName.length > 32) { - throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); - } - return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); - }, - - /** - * Updates the proxy for a member, first checking that no other members attached to the author have the tag. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise } A successful update. - * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. - */ - async updateProxy (authorId, args) { - if (args[2] && args[2] === "--help") { - return enums.help.PROXY; - } - const proxyExists = await this.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { - return proxyExists; - }).catch((e) =>{throw e}); - if (!proxyExists) { - return await this.updateMemberField(authorId, args).catch((e) =>{throw e}); - } - }, - - /** - * Updates the profile pic for a member, based on either the attachment or the args provided. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @param {string} attachmentUrl - The url of the first attachment in the message - * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) - * @returns {Promise} A successful update. - * @throws {Error} When loading the profile picture from a URL doesn't work. - */ - async updatePropic (authorId, args, attachmentUrl, attachmentExpiry= null) { - if (args[1] && args[1] === "--help") { - return enums.help.PROPIC; + const name = args[2]; + const trimmedName = name ? name.trim() : null; + if (!name || trimmedName === null) { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + } + return await mh.updateMemberField(authorId, args).catch((e) => { + throw e + }); +} + +/** + * Updates the display name for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {RangeError} When the display name is too long or doesn't exist. + */ +mh.updateDisplayName = async function (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.DISPLAY_NAME; + } + + const memberName = args[0]; + const displayName = args[2]; + const trimmedName = displayName ? displayName.trim() : null; + + if (!displayName || trimmedName === null) { + return await mh.getMemberByName(authorId, memberName).then((member) => { + if (member && member.displayname) { + return `Display name for ${memberName} is: \"${member.displayname}\".`; + } else if (member) { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + } + }); + } else if (displayName.length > 32) { + throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); + } + return await mh.updateMemberField(authorId, args).catch((e) => { + throw e + }); +} + +/** + * Updates the proxy for a member, first checking that no other members attached to the author have the tag. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise } A successful update. + * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. + */ +mh.updateProxy = async function (authorId, args) { + if (args[2] && args[2] === "--help") { + return enums.help.PROXY; + } + const proxyExists = await mh.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { + return proxyExists; + }).catch((e) => { + throw e + }); + if (!proxyExists) { + return await mh.updateMemberField(authorId, args).catch((e) => { + throw e + }); + } +} + +/** + * Updates the profile pic for a member, based on either the attachment or the args provided. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @param {string} attachmentUrl - The url of the first attachment in the message + * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) + * @returns {Promise} A successful update. + * @throws {Error} When loading the profile picture from a URL doesn't work. + */ +mh.updatePropic = async function (authorId, args, attachmentUrl, attachmentExpiry = null) { + if (args[1] && args[1] === "--help") { + return enums.help.PROPIC; + } + let img; + const updatedArgs = args; + if (!updatedArgs[1] && !attachmentUrl) { + return enums.help.PROPIC; + } else if (attachmentUrl) { + updatedArgs[2] = attachmentUrl; + updatedArgs[3] = attachmentExpiry; + } + if (updatedArgs[2]) { + img = updatedArgs[2]; + } + const isValidImage = await mh.checkImageFormatValidity(img).catch((e) => { + throw e + }); + if (isValidImage) { + return await mh.updateMemberField(authorId, updatedArgs).catch((e) => { + throw e + }); + } +} + +/** + * Checks if an uploaded picture is in the right format. + * + * @async + * @param {string} imageUrl - The url of the image + * @returns {Promise} - If the image is a valid format. + * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. + */ +mh.checkImageFormatValidity = async function (imageUrl) { + const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; + return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { + if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); + return true; + }).catch((error) => { + throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); + }); +} + +/** + * Removes a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful removal. + * @throws {EmptyResultError} When there is no member to remove. + */ +mh.removeMember = async function (authorId, args) { + if (args[1] && args[1] === "--help" || !args[1]) { + return enums.help.REMOVE; + } + + const memberName = args[1]; + return await database.members.destroy({ + where: { + name: {[Op.iLike]: memberName}, + userid: authorId } - let img; - const updatedArgs = args; - if (!updatedArgs[1] && !attachmentUrl) { - return enums.help.PROPIC; - } else if (attachmentUrl) { - updatedArgs[2] = attachmentUrl; - updatedArgs[3] = attachmentExpiry; + }).then((result) => { + if (result) { + return `Member "${memberName}" has been deleted.`; } - if (updatedArgs[2]) { - img = updatedArgs[2]; + throw new EmptyResultError(`${enums.err.NO_MEMBER}`); + }) +} + +/*======Non-Subcommands======*/ + +/** + * Adds a member with full details, first checking that there is no member of that name associated with the author. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} memberName - The name of the member. + * @param {string | null} displayName - The display name of the member. + * @param {string | null} proxy - The proxy tag of the member. + * @param {string | null} propic - The profile picture URL of the member. + * @param {boolean} isImport - Whether calling from the import function or not. + * @returns {Promise} A successful addition. + * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. + */ +mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, isImport = false) { + await mh.getMemberByName(authorId, memberName).then((member) => { + if (member) { + throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); } - const isValidImage = await this.checkImageFormatValidity(img).catch((e) =>{throw e}); - if (isValidImage) { - return await this.updateMemberField(authorId, updatedArgs).catch((e) =>{throw e}); + }); + if (displayName) { + const trimmedName = displayName ? displayName.trim() : null; + if (trimmedName && trimmedName.length > 32) { + throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); } - }, - - /** - * Checks if an uploaded picture is in the right format. - * - * @async - * @param {string} imageUrl - The url of the image - * @returns {Promise} - If the image is a valid format. - * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. - */ - async checkImageFormatValidity (imageUrl) { - const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; - return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { - if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); - return true; - }).catch((error) => { - throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); + } + if (proxy) { + await mh.checkIfProxyExists(authorId, proxy).catch((e) => { + throw e }); - }, - - /** - * Removes a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful removal. - * @throws {EmptyResultError} When there is no member to remove. - */ - async removeMember (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.REMOVE; - } - - const memberName = args[1]; - return await database.members.destroy({ where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then((result) => { - if (result) { - return `Member "${memberName}" has been deleted.`; - } - throw new EmptyResultError(`${enums.err.NO_MEMBER}`); - }) - }, - - /*======Non-Subcommands======*/ - - /** - * Adds a member with full details, first checking that there is no member of that name associated with the author. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The name of the member. - * @param {string | null} displayName - The display name of the member. - * @param {string | null} proxy - The proxy tag of the member. - * @param {string | null} propic - The profile picture URL of the member. - * @param {boolean} isImport - Whether calling from the import or not. - * @returns {Promise} A successful addition. - * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. - */ - async addFullMember (authorId, memberName, displayName = null, proxy = null, propic= null, isImport = false) { - await this.getMemberByName(authorId, memberName).then((member) => { - if (member) { - throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); + } + let validPropic; + if (propic) { + validPropic = await mh.checkImageFormatValidity(propic).then((valid) => { + return valid; + }).catch((e) => { + if (!isImport) { + throw (e); } + return false; }); - if (displayName) { - const trimmedName = displayName ? displayName.trim() : null; - if (trimmedName && trimmedName.length > 32) { - throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); - } - } - if (proxy) { - await this.checkIfProxyExists(authorId, proxy).catch((e) =>{throw e}); + } + + const member = await database.members.create({ + name: memberName, userid: authorId, displayname: displayName, proxy: proxy, propic: validPropic ? propic : null, + }); + if (!member) { + new Error(`${enums.err.ADD_ERROR}`); + } +} + +/** + * Updates one fields for a member in the database. + * + * @async + * @param {string} authorId - The author of the message + * @param {string[]} args - The message arguments + * @returns {Promise} A successful update. + * @throws {EmptyResultError | Error} When the member is not found, or catchall error. + */ +mh.updateMemberField = async function (authorId, args) { + const memberName = args[0]; + const columnName = args[1]; + const value = args[2]; + let fluxerPropicWarning; + + // indicates that an attachment was uploaded on Fluxer directly + if (columnName === "propic" && args[3]) { + fluxerPropicWarning = mh.setExpirationWarning(args[3]); + } + return await database.members.update({[columnName]: value}, { + where: { + name: {[Op.iLike]: memberName}, + userid: authorId } - let validPropic; - if (propic) { - validPropic = await this.checkImageFormatValidity(propic).then((valid) => { - return valid; - }).catch((e) =>{ - if (!isImport) { - throw (e); - } - return false; - }); + }).then(() => { + return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; + }).catch(e => { + if (e === EmptyResultError) { + throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); + } else { + throw new Error(`Can't update ${memberName}. ${e.message}`); } + }); +} - const member = await database.members.create({ - name: memberName, - userid: authorId, - displayname: displayName, - proxy: proxy, - propic: validPropic ? propic : null, - }); - if (!member) { - new Error(`${enums.err.ADD_ERROR}`); - } - }, - - /** - * Updates one fields for a member in the database. - * - * @async - * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @returns {Promise} A successful update. - * @throws {EmptyResultError | Error} When the member is not found, or catchall error. - */ - async updateMemberField (authorId, args) { - const memberName = args[0]; - const columnName = args[1]; - const value = args[2]; - let fluxerPropicWarning; - - // indicates that an attachment was uploaded on Fluxer directly - if (columnName === "propic" && args[3]) { - fluxerPropicWarning = this.setExpirationWarning(args[3]); +/** + * Sets the warning for an expiration date. + * + * @param {string} expirationString - An expiration date string. + * @returns {string} A description of the expiration, interpolating the expiration string. + */ +mh.setExpirationWarning = function (expirationString) { + let expirationDate = new Date(expirationString); + if (!isNaN(expirationDate.valueOf())) { + expirationDate = expirationDate.toDateString(); + return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like and link to it directly` + } +} + +/** + * Gets the details for a member. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} memberName - The message arguments + * @returns {Promise} The member's info. + */ +mh.getMemberInfo = async function (authorId, memberName) { + return await mh.getMemberByName(authorId, memberName).then((member) => { + if (member) { + return new EmbedBuilder() + .setTitle(member.name) + .setDescription(`Details for ${member.name}`) + .addFields({ + name: 'Display name: ', + value: member.displayname ?? 'unset', + inline: true + }, {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true},) + .setImage(member.propic); } - return await database.members.update({[columnName]: value}, { where: { name: {[Op.iLike]: memberName}, userid: authorId } }).then(() => { - return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; - }).catch(e => { - if (e === EmptyResultError) { - throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); - } - else { - throw new Error(`Can't update ${memberName}. ${e.message}`); - } - }); - }, - - /** - * Sets the warning for an expiration date. - * - * @param {string} expirationString - An expiration date string. - * @returns {string} A description of the expiration, interpolating the expiration string. - */ - setExpirationWarning(expirationString) { - let expirationDate = new Date(expirationString); - if (!isNaN(expirationDate.valueOf())) { - expirationDate = expirationDate.toDateString(); - return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like and link to it directly` + }); +} + +/** + * Gets all members for an author. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorName - The id name the message author + * @returns {Promise} The info for all members. + * @throws {Error} When there are no members for an author. + */ +mh.getAllMembersInfo = async function (authorId, authorName) { + const members = await mh.getMembersByAuthor(authorId); + if (members == null) throw Error(enums.err.USER_NO_MEMBERS); + const fields = [...members.entries()].map(([name, member]) => ({ + name: member.name, value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, inline: true, + })); + return new EmbedBuilder() + .setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`) + .addFields(...fields); +} + +/** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message. + * @param {string} memberName - The member's name. + * @returns {Promise} The member object. + * @throws { EmptyResultError } When the member is not found. + */ +mh.getMemberByName = async function (authorId, memberName) { + return await database.members.findOne({where: {userid: authorId, name: {[Op.iLike]: memberName}}}); +} + +/** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message. + * @param {string} memberName - The member's name. + * @returns {Promise} The member object. + * @throws { EmptyResultError } When the member is not found. + */ +mh.getProxyByMember = async function (authorId, memberName) { + return await mh.getMemberByName(authorId, memberName).then((member) => { + if (member) { + return member.dataValues.proxy; } - }, - - /** - * Gets the details for a member. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The message arguments - * @returns {Promise} The member's info. - */ - async getMemberInfo (authorId, memberName) { - return await this.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return new EmbedBuilder() - .setTitle(member.name) - .setDescription(`Details for ${member.name}`) - .addFields( - {name: 'Display name: ', value: member.displayname ?? 'unset', inline: true}, - {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true}, - ) - .setImage(member.propic); + throw new EmptyResultError(enums.err.NO_MEMBER); + }) +} + +/** + * Gets a member based on the author and proxy tag. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} proxy - The proxy tag + * @returns {Promise} The member object. + */ +mh.getMemberByProxy = async function (authorId, proxy) { + return await db.members.findOne({where: {userid: authorId, proxy: proxy}}); +} + +/** + * Gets all members belonging to the author. + * + * @async + * @param {string} authorId - The author of the message + * @returns {Promise} The member object array. + */ +mh.getMembersByAuthor = async function (authorId) { + return await database.members.findAll({where: {userid: authorId}}); +} + + +/** + * Checks if proxy exists for a member. + * + * @param {string} authorId - The author of the message + * @param {string} proxy - The proxy tag. + * @returns {Promise } Whether the proxy exists. + * @throws {Error} When an empty proxy was provided, or no proxy exists. + */ +mh.checkIfProxyExists = async function (authorId, proxy) { + if (proxy) { + const splitProxy = proxy.trim().split("text"); + if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); + if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); + + await mh.getMembersByAuthor(authorId).then((memberList) => { + const proxyExists = memberList.some(member => member.proxy === proxy); + if (proxyExists) { + throw new Error(enums.err.PROXY_EXISTS); } + }).catch(e => { + throw e }); - }, - - /** - * Gets all members for an author. - * - * @async - * @param {string} authorId - The id of the message author - * @param {string} authorName - The id name the message author - * @returns {Promise} The info for all members. - * @throws {Error} When there are no members for an author. - */ - async getAllMembersInfo (authorId, authorName) { - const members = await this.getMembersByAuthor(authorId); - if (members == null) throw Error(enums.err.USER_NO_MEMBERS); - const fields = [...members.entries()].map(([name, member]) => ({ - name: member.name, - value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, - inline: true, - })); - return new EmbedBuilder() - .setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`) - .addFields(...fields); - }, - - /** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ - async getMemberByName (authorId, memberName) { - return await database.members.findOne({ where: { userid: authorId, name: {[Op.iLike]: memberName}}}); - }, - - /** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ - async getProxyByMember (authorId, memberName) { - return await this.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return member.dataValues.proxy; - } - throw new EmptyResultError(enums.err.NO_MEMBER); - }) - }, - - /** - * Gets all members belonging to the author. - * - * @async - * @param {string} authorId - The author of the message - * @returns {Promise} The member object array. - */ - async getMembersByAuthor (authorId) { - return await database.members.findAll({ where: { userid: authorId } }); - }, - - - /** - * Checks if proxy exists for a member. - * - * @param {string} authorId - The author of the message - * @param {string} proxy - The proxy tag. - * @returns {Promise } Whether the proxy exists. - * @throws {Error} When an empty proxy was provided, or no proxy exists. - */ - async checkIfProxyExists (authorId, proxy) { - if (proxy) { - const splitProxy = proxy.trim().split("text"); - if(splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); - if(!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); - - await this.getMembersByAuthor(authorId).then((memberList) => { - const proxyExists = memberList.some(member => member.proxy === proxy); - if (proxyExists) { - throw new Error(enums.err.PROXY_EXISTS); - } - }).catch(e =>{throw e}); - } - } + } -module.exports = memberHelper; \ No newline at end of file +export const memberHelper = mh; \ No newline at end of file diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index c297130..94a200a 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -1,96 +1,95 @@ -const {memberHelper} = require('memberHelper.js'); -const {fs} = require('fs') -const {tmp, setGracefulCleanup } = require('tmp') -const {enums} = require('../enums.js') -const {Message} = require('@fluxerjs/core'); +import {memberHelper} from "./memberHelper.js"; +import {enums} from "../enums.js"; +import tmp, {setGracefulCleanup} from "tmp"; +import fs from 'fs'; +import {Message} from "@fluxerjs/core"; -setGracefulCleanup(); +const msgh = {}; -const messageHelper = { +msgh.prefix = "pf;" - prefix: "pf;", - - /** - * Parses and slices up message arguments, retaining quoted strings. - * - * @param {string} content - The full message content. - * @param {string} commandName - The command name. - * @returns {string[]} An array of arguments. - */ - parseCommandArgs(content, commandName) { - const message = content.slice(this.prefix.length + commandName.length).trim(); +setGracefulCleanup(); - return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { - if (chara === '"') { - // checks whether string is within quotes or not - accumulator.quote ^= 1; - } else if (!accumulator.quote && chara === ' ') { - // if not currently in quoted string, push empty string to start word - accumulator.array.push(''); - } else { - // accumulates characters to the last string in the array and removes escape characters - accumulator.array[accumulator.array.length - 1] += chara.replace(/\\(.)/, "$1"); - } - return accumulator; - }, {array: ['']}).array // initial array with empty string for the reducer - }, +/** + * Parses and slices up message arguments, retaining quoted strings. + * + * @param {string} content - The full message content. + * @param {string} commandName - The command name. + * @returns {string[]} An array of arguments. + */ +msgh.parseCommandArgs = function(content, commandName) { + const message = content.slice(msgh.prefix.length + commandName.length).trim(); - /** - * Parses messages to see if any part of the text matches the tags of any member belonging to an author. - * - * @param {string} authorId - The author of the message. - * @param {string} content - The full message content - * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. - * @returns {Object} The proxy message object. - * @throws {Error} If a proxy message is sent with no message within it. - */ - async parseProxyTags(authorId, content, attachmentUrl = null) { - const members = await memberHelper.getMembersByAuthor(authorId); - // If an author has no members, no sense in searching for proxy - if (members.length === 0) { - return; + return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { + if (chara === '"') { + // checks whether string is within quotes or not + accumulator.quote ^= 1; + } else if (!accumulator.quote && chara === ' '){ + // if not currently in quoted string, push empty string to start word + accumulator.array.push(''); + } else { + // accumulates characters to the last string in the array and removes escape characters + accumulator.array[accumulator.array.length-1] += chara.replace(/\\(.)/,"$1"); } + return accumulator; + }, {array: ['']}).array // initial array with empty string for the reducer +} - const proxyMessage = {} - members.forEach(member => { - if (member.proxy) { - const splitProxy = member.proxy.split("text"); - if (content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { - proxyMessage.member = member; - if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; +/** + * Parses messages to see if any part of the text matches the tags of any member belonging to an author. + * + * @param {string} authorId - The author of the message. + * @param {string} content - The full message content + * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. + * @returns {Object} The proxy message object. + * @throws {Error} If a proxy message is sent with no message within it. + */ +msgh.parseProxyTags = async function (authorId, content, attachmentUrl= null){ + const members = await memberHelper.getMembersByAuthor(authorId); + // If an author has no members, no sense in searching for proxy + if (members.length === 0) { + return; + } - let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - escapedPrefix = new RegExp("^" + escapedPrefix); - escapedSuffix = new RegExp(escapedSuffix + "$") - proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); - if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); - } + const proxyMessage = {} + members.forEach(member => { + if (member.proxy) { + const splitProxy = member.proxy.split("text"); + if(content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { + proxyMessage.member = member; + if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; + + let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + escapedPrefix = new RegExp("^" + escapedPrefix); + escapedSuffix = new RegExp(escapedSuffix + "$") + proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); + if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); } - }) - return proxyMessage; - }, + } + }) + return proxyMessage; +} - /** - * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE - * - * @async - * @param {string} text - The text of the message. - * @param {Message} message - The message object. - * @throws {Error} If a proxy message is sent with no message within it. - * - */ - async sendMessageAsAttachment(text, message) { - if (text.length > 2000) { - tmp.file(async (err, path, fd, cleanupCallback) => { - fs.writeFile(path, text, (err) => { - if (err) throw err; - }) +/** + * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE + * + * @async + * @param {string} text - The text of the message. + * @param {Message} message - The message object. + * @throws {Error} If a proxy message is sent with no message within it. + * + */ +msgh.sendMessageAsAttachment = async function(text, message) { + if (text.length > 2000) { + tmp.file(async (err, path, fd, cleanupCallback) => { + fs.writeFile(path, text, (err) => { if (err) throw err; - await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); - }); - } + }) + if (err) throw err; + await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); + }); } -}; +} -module.exports = messageHelper; +export const messageHelper = msgh; diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index 92158b8..dd78184 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -1,101 +1,93 @@ -const {messageHelper} = require('messageHelper.js'); -const {enums} = require('../enums.js'); -const {Client, Message, Webhook, Channel} = require('@fluxerjs/core'); +import {messageHelper} from "./messageHelper.js"; +import {Webhook, Channel, Message} from '@fluxerjs/core'; +import {enums} from "../enums.js"; -const name = 'PluralFlux Proxy Webhook'; +const wh = {}; -const webhookHelper = { - /** - * Replaces a proxied message with a webhook using the member information. - * @param {Client} client - The fluxer.js client. - * @param {Message} message - The full message object. - * @throws {Error} When the proxy message is not in a server. - */ - async sendMessageAsMember(client, message) { - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; - const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e => { - throw e - }); - // If the message doesn't match a proxy, just return. - if (!proxyMatch.member) { - return; - } - // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs - if (!message.guildId) { - throw new Error(enums.err.NOT_IN_SERVER); - } +const name = 'PluralFlux Proxy Webhook'; - if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { - return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) - } - await this.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e => { - throw e - }); - }, +/** + * Replaces a proxied message with a webhook using the member information. + * @param {Client} client - The fluxer.js client. + * @param {Message} message - The full message object. + * @throws {Error} When the proxy message is not in a server. + */ +wh.sendMessageAsMember = async function(client, message) { + const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; + const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e}); + // If the message doesn't match a proxy, just return. + if (!proxyMatch.member) { + return; + } + // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs + if (!message.guildId) { + throw new Error(enums.err.NOT_IN_SERVER); + } - /** - * Replaces a proxied message with a webhook using the member information. - * @param {Client} client - The fluxer.js client. - * @param {Message} message - The message to be deleted. - * @param {string} text - The text to send via the webhook. - * @param {model} member - A member object from the database. - * @throws {Error} When there's no message to send. - */ - async replaceMessage(client, message, text, member) { - if (text.length > 0 || message.attachments.size > 0) { - const channel = client.channels.get(message.channelId); - const webhook = await this.getOrCreateWebhook(client, channel).catch((e) => { - throw e - }); - const username = member.displayname ?? member.name; - await webhook.send({content: text, username: username, avatar_url: member.propic}); - await message.delete(); - } else { - throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); - } - }, + if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { + return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) + } + await replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); +} - /** - * Gets or creates a webhook. - * - * @param {Client} client - The fluxer.js client. - * @param {Channel} channel - The channel the message was sent in. - * @returns {Webhook} A webhook object. - * @throws {Error} When no webhooks are allowed in the channel. - */ - async getOrCreateWebhook(client, channel) { - // If channel doesn't allow webhooks - if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); - let webhook = await this.getWebhook(client, channel).catch((e) => { - throw e - }); - if (!webhook) { - webhook = await channel.createWebhook({name: name}); - } - return webhook; - }, +/** + * Replaces a proxied message with a webhook using the member information. + * @param {Client} client - The fluxer.js client. + * @param {Message} message - The message to be deleted. + * @param {string} text - The text to send via the webhook. + * @param {model} member - A member object from the database. + * @throws {Error} When there's no message to send. + */ +async function replaceMessage(client, message, text, member) { + if (text.length > 0 || message.attachments.size > 0) { + const channel = client.channels.get(message.channelId); + const webhook = await getOrCreateWebhook(client, channel).catch((e) =>{throw e}); + const username = member.displayname ?? member.name; + await webhook.send({content: text, username: username, avatar_url: member.propic}); + await message.delete(); + } + else { + throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + } +} - /** - * Gets an existing webhook. - * - * @param {Client} client - The fluxer.js client. - * @param {Channel} channel - The channel the message was sent in. - * @returns {Webhook} A webhook object. - */ - async getWebhook(client, channel) { - const channelWebhooks = await channel?.fetchWebhooks() ?? []; - if (channelWebhooks.length === 0) { - return; - } - let pf_webhook; - channelWebhooks.forEach((webhook) => { - if (webhook.name === name) { - pf_webhook = webhook; - } - }) - return pf_webhook; +/** + * Gets or creates a webhook. + * + * @param {Client} client - The fluxer.js client. + * @param {Channel} channel - The channel the message was sent in. + * @returns {Webhook} A webhook object. + * @throws {Error} When no webhooks are allowed in the channel. + */ +async function getOrCreateWebhook(client, channel) { + // If channel doesn't allow webhooks + if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); + let webhook = await getWebhook(client, channel).catch((e) =>{throw e}); + if (!webhook) { + webhook = await channel.createWebhook({name: name}); } + return webhook; } +/** + * Gets an existing webhook. + * + * @param {Client} client - The fluxer.js client. + * @param {Channel} channel - The channel the message was sent in. + * @returns {Webhook} A webhook object. + */ +async function getWebhook(client, channel) { + const channelWebhooks = await channel?.fetchWebhooks() ?? []; + if (channelWebhooks.length === 0) { + return; + } + let pf_webhook; + channelWebhooks.forEach((webhook) => { + if (webhook.name === name) { + pf_webhook = webhook; + } + }) + return pf_webhook; +} -module.exports = webhookHelper; \ No newline at end of file +export const webhookHelper = wh; \ No newline at end of file diff --git a/src/helpers/importHelper.js b/src/import.js similarity index 56% rename from src/helpers/importHelper.js rename to src/import.js index 1c0f8c2..feee479 100644 --- a/src/helpers/importHelper.js +++ b/src/import.js @@ -1,22 +1,22 @@ -const {enums} = require("../enums.js"); -const {memberHelper} = require("memberHelper.js"); +import {enums} from "./enums.js"; +import {memberHelper} from "./helpers/memberHelper.js"; -const importHelper = { +const ih = {}; - /** - * Tries to import from Pluralkit. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} attachmentUrl - The attached JSON url. - * @returns {string} A successful addition of all members. - * @throws {Error} When the member exists, or creating a member doesn't work. - */ - async pluralKitImport(authorId, attachmentUrl) { - if (!attachmentUrl) { - throw new Error(enums.err.NOT_JSON_FILE); - } - return fetch(attachmentUrl).then((res) => res.json()).then(async (pkData) => { +/** + * Tries to import from Pluralkit. + * + * @async + * @param {string} authorId - The author of the message + * @param {string} attachmentUrl - The attached JSON url. + * @returns {string} A successful addition of all members. + * @throws {Error} When the member exists, or creating a member doesn't work. + */ +ih.pluralKitImport = async function (authorId, attachmentUrl) { + if (!attachmentUrl) { + throw new Error(enums.err.NOT_JSON_FILE); + } + return fetch(attachmentUrl).then((res) => res.json()).then(async(pkData) => { const pkMembers = pkData.members; const errors = []; const addedMembers = []; @@ -28,8 +28,7 @@ const importHelper = { errors.push(`${pkMember.name}: ${e.message}`); }); await memberHelper.checkImageFormatValidity(pkMember.avatar_url).catch(e => { - errors.push(`${pkMember.name}: ${e.message}`) - }); + errors.push(`${pkMember.name}: ${e.message}`)}); } const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED; if (errors.length > 0) { @@ -37,7 +36,6 @@ const importHelper = { } return aggregatedText; }); - } -}; +} -module.exports = importHelper; \ No newline at end of file +export const importHelper = ih; \ No newline at end of file From ba9552b4aa94e7b27f58aba25bd40eb904495547 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 17:38:06 -0500 Subject: [PATCH 11/59] updated jest to sort of work with es6 --- package.json | 12 ++++++++--- tests/env.jest | 2 ++ ...ersHelper.test.js => memberHelper.test.js} | 20 ++++++++++--------- tests/jest.config.js | 11 ++++++++++ tests/jest.setup.js | 2 ++ 5 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 tests/env.jest rename tests/helpers/{membersHelper.test.js => memberHelper.test.js} (80%) create mode 100644 tests/jest.config.js create mode 100644 tests/jest.setup.js diff --git a/package.json b/package.json index 5cf0fb8..eea7e3a 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,21 @@ "jest": "^30.2.0" }, "scripts": { - "test": "jest" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "jest": { "testEnvironment": "node", "coveragePathIgnorePatterns": [ "/node_modules/" ], - "moduleFileExtensions": ["js", "json"], - "testMatch": ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"], + "moduleFileExtensions": [ + "js", + "json" + ], + "testMatch": [ + "**/__tests__/**/*.js", + "**/?(*.)+(spec|test).js" + ], "verbose": true } } diff --git a/tests/env.jest b/tests/env.jest new file mode 100644 index 0000000..c130787 --- /dev/null +++ b/tests/env.jest @@ -0,0 +1,2 @@ +FLUXER_BOT_TOKEN=jest-fluxer-bot-token +POSTGRES_PASSWORD=jest-postgres-password \ No newline at end of file diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/memberHelper.test.js similarity index 80% rename from tests/helpers/membersHelper.test.js rename to tests/helpers/memberHelper.test.js index b3ee50c..5d27624 100644 --- a/tests/helpers/membersHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -1,12 +1,14 @@ -jest.mock('@fluxerjs/core', () => jest.fn()); -jest.mock('../../src/db.js', () => jest.fn()); -jest.mock('sequelize', () => jest.fn()); -jest.mock('../../src/enums.js', () => ({ - enums: jest.requireActual('../../src/enums.js') -})); - -const {enums} = require("../../src/enums.js"); -const memberHelper = require("../../src/helpers/memberHelper.js"); +import {jest} from "@jest/globals"; + +jest.unstable_mockModule('@fluxerjs/core', () => jest.fn()); +jest.unstable_mockModule('../../src/db.js', () => jest.fn()); +jest.unstable_mockModule('sequelize', () => jest.fn()); + +const { EmbedBuilder } = await import ("@fluxerjs/core"); +const { database } = await import('../../src/db.js'); +const { EmptyResultError, Op } = await import ('sequelize'); +import { enums } from "../../src/enums.js"; +import { memberHelper } from "../../src/helpers/memberHelper.js"; describe('parseMemberCommand', () => { diff --git a/tests/jest.config.js b/tests/jest.config.js new file mode 100644 index 0000000..bdee2b6 --- /dev/null +++ b/tests/jest.config.js @@ -0,0 +1,11 @@ +/** @type {import('jest').Config} */ +const config = { + clearMocks: true, + collectCoverage: true, + coverageDirectory: "coverage", + setupFiles: ["/jest.setup.js"], // path to a setup module to configure the testing environment before each test + transform: {}, + verbose: true, +}; + +export default config; diff --git a/tests/jest.setup.js b/tests/jest.setup.js new file mode 100644 index 0000000..f44af1b --- /dev/null +++ b/tests/jest.setup.js @@ -0,0 +1,2 @@ +import * as dotenv from "dotenv"; +dotenv.config({ path: "env.jest" }); From 4fcb53482cf7e647b8cfcec1454aa8d419d79617 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 19:36:14 -0500 Subject: [PATCH 12/59] separating out enum return from method return --- .gitignore | 3 +- tests/helpers/memberHelper.test.js | 130 +++++++++++++++++------------ 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 9675deb..fa6eef6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ package-lock.json config.json coverage log.txt -.env \ No newline at end of file +.env +oya.png \ No newline at end of file diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 5d27624..b2505f7 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -4,58 +4,82 @@ jest.unstable_mockModule('@fluxerjs/core', () => jest.fn()); jest.unstable_mockModule('../../src/db.js', () => jest.fn()); jest.unstable_mockModule('sequelize', () => jest.fn()); -const { EmbedBuilder } = await import ("@fluxerjs/core"); -const { database } = await import('../../src/db.js'); -const { EmptyResultError, Op } = await import ('sequelize'); -import { enums } from "../../src/enums.js"; -import { memberHelper } from "../../src/helpers/memberHelper.js"; - -describe('parseMemberCommand', () => { - - beforeEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); - jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); - jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); - jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); - jest.spyOn(memberHelper, 'updateName').mockResolvedValue("update name"); - jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); - jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); - jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); - jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("update proxy"); - }); - - - test.each([ - [['--help'], enums.help.MEMBER], - [['new'], 'new member'], - [['remove'], 'remove member'], - [['name'], enums.help.NAME], - [['displayname'], enums.help.DISPLAY_NAME], - [['proxy'], enums.help.PROXY], - [['propic'], enums.help.PROPIC], - [['list'], 'all member info'], - [[''], enums.help.MEMBER], - [['somePerson', 'new'], enums.help.NEW], - [['somePerson', 'name'], 'update name'], - [['somePerson', 'displayname'], 'update display name'], - [['somePerson', 'proxy'], 'update proxy'], - [['somePerson', 'propic'], 'update propic'], - [['somePerson'], 'member info'], - ])('%s returns correct values', async(args, expectedResult) => { - // Arrange - const authorId = '1'; - const authorFull = 'somePerson#0001'; - // Act - const result = await memberHelper.parseMemberCommand(authorId, authorFull, args); - // - expect(result).toEqual(expectedResult); - }); - - afterEach(() => { - // restore the spy created with spyOn - jest.restoreAllMocks(); - }); +const {EmbedBuilder} = await import ("@fluxerjs/core"); +const {database} = await import('../../src/db.js'); +const {EmptyResultError, Op} = await import ('sequelize'); +import {enums} from "../../src/enums.js"; +import {memberHelper} from "../../src/helpers/memberHelper.js"; +import * as test from "node:test"; + +describe('MemberHelper', () => { + const authorId = "0001"; + const authorFull = "author#0001"; + const attachmentUrl = "../oya.png"; + const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + + describe('parseMemberCommand', () => { + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); + jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); + jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); + jest.spyOn(memberHelper, 'updateName').mockResolvedValue("update name"); + jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); + jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); + jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); + jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("update proxy"); + }); + + + test.each([ + // [['--help'], enums.help.MEMBER], + [['new'], 'new member', memberHelper.addNewMember, [authorId, ['new']]], + [['remove'], 'remove member', memberHelper.removeMember, [authorId, ['remove']]], + [['list'], 'all member info', memberHelper.getAllMembersInfo, [authorId, ['list']]], + [['somePerson', 'name'], 'update name', memberHelper.updateName, [authorId, ['somePerson', 'name']]], + [['somePerson', 'displayname'], 'update display name', memberHelper.updateDisplayName, [authorId, ['somePerson', 'displayname']]], + [['somePerson', 'proxy'], 'update proxy', memberHelper.addNewMember, [authorId, 'somePerson']], + [['somePerson', 'proxy', 'test'], 'update proxy', memberHelper.addNewMember, [authorId, ['somePerson', 'proxy', 'test']]], + [['somePerson', 'propic'], 'update propic', memberHelper.updatePropic, [authorId, ['somePerson', 'propic']]], + [['somePerson'], 'member info', memberHelper.getMemberInfo, [authorId, 'somePerson']], + ])('%s returns correct values', async (args, expectedResult, method, passedIn) => { + // Arrange + const authorId = '1'; + const authorFull = 'somePerson#0001'; + // Act + memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { + expect(result).toEqual(expectedResult); + expect(method).toHaveBeenCalledTimes(1); + expect(method).toHaveBeenCalledWith(passedIn) + }); + }); + + test.each([ + [['--help'], enums.help.MEMBER], + [['name'], enums.help.NAME], + [['displayname'], enums.help.DISPLAY_NAME], + [['proxy'], enums.help.PROXY], + [['propic'], enums.help.PROPIC], + [['list', '--help'], enums.help.LIST], + [[''], enums.help.MEMBER], + ])('%s returns correct values', async (args, expectedResult, method, passedIn) => { + // Arrange + const authorId = '1'; + const authorFull = 'somePerson#0001'; + // Act + memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { + expect(result).toEqual(expectedResult); + }); + }); + + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); + }) }) From 5e3b3f33d3b3b806860ed5521bf32999bab10db9 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 19:38:50 -0500 Subject: [PATCH 13/59] mostly working except for the weirdest error --- tests/helpers/memberHelper.test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index b2505f7..68e9619 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -9,7 +9,6 @@ const {database} = await import('../../src/db.js'); const {EmptyResultError, Op} = await import ('sequelize'); import {enums} from "../../src/enums.js"; import {memberHelper} from "../../src/helpers/memberHelper.js"; -import * as test from "node:test"; describe('MemberHelper', () => { const authorId = "0001"; @@ -35,7 +34,7 @@ describe('MemberHelper', () => { test.each([ - // [['--help'], enums.help.MEMBER], + [['--help'], enums.help.MEMBER], [['new'], 'new member', memberHelper.addNewMember, [authorId, ['new']]], [['remove'], 'remove member', memberHelper.removeMember, [authorId, ['remove']]], [['list'], 'all member info', memberHelper.getAllMembersInfo, [authorId, ['list']]], @@ -65,7 +64,7 @@ describe('MemberHelper', () => { [['propic'], enums.help.PROPIC], [['list', '--help'], enums.help.LIST], [[''], enums.help.MEMBER], - ])('%s returns correct values', async (args, expectedResult, method, passedIn) => { + ])('%s returns correct enums', async (args, expectedResult) => { // Arrange const authorId = '1'; const authorFull = 'somePerson#0001'; From 164ff7d8b675ae4b221a94dace5d41cbf89f134e Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 19:58:56 -0500 Subject: [PATCH 14/59] nevermind it wasn't actually working, gonna move on for now --- tests/helpers/memberHelper.test.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 68e9619..f8b44ca 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -1,8 +1,8 @@ import {jest} from "@jest/globals"; -jest.unstable_mockModule('@fluxerjs/core', () => jest.fn()); -jest.unstable_mockModule('../../src/db.js', () => jest.fn()); -jest.unstable_mockModule('sequelize', () => jest.fn()); +jest.mock('@fluxerjs/core', () => jest.fn()); +jest.mock('../../src/db.js', () => jest.fn()); +jest.mock('sequelize', () => jest.fn()); const {EmbedBuilder} = await import ("@fluxerjs/core"); const {database} = await import('../../src/db.js'); @@ -16,6 +16,7 @@ describe('MemberHelper', () => { const attachmentUrl = "../oya.png"; const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + describe('parseMemberCommand', () => { beforeEach(() => { @@ -29,27 +30,25 @@ describe('MemberHelper', () => { jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); - jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("update proxy"); + jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("get proxy"); }); - test.each([ - [['--help'], enums.help.MEMBER], + test.skip.each([ [['new'], 'new member', memberHelper.addNewMember, [authorId, ['new']]], [['remove'], 'remove member', memberHelper.removeMember, [authorId, ['remove']]], [['list'], 'all member info', memberHelper.getAllMembersInfo, [authorId, ['list']]], [['somePerson', 'name'], 'update name', memberHelper.updateName, [authorId, ['somePerson', 'name']]], [['somePerson', 'displayname'], 'update display name', memberHelper.updateDisplayName, [authorId, ['somePerson', 'displayname']]], - [['somePerson', 'proxy'], 'update proxy', memberHelper.addNewMember, [authorId, 'somePerson']], - [['somePerson', 'proxy', 'test'], 'update proxy', memberHelper.addNewMember, [authorId, ['somePerson', 'proxy', 'test']]], + [['somePerson', 'proxy'], 'get proxy', memberHelper.getProxyByMember, [authorId, 'somePerson']], + [['somePerson', 'proxy', 'test'], 'update proxy', memberHelper.updateProxy, [authorId, ['somePerson', 'proxy', 'test']]], [['somePerson', 'propic'], 'update propic', memberHelper.updatePropic, [authorId, ['somePerson', 'propic']]], - [['somePerson'], 'member info', memberHelper.getMemberInfo, [authorId, 'somePerson']], + [['somePerson'], 'member info', getMemberInfoMock, [authorId, 'somePerson']], ])('%s returns correct values', async (args, expectedResult, method, passedIn) => { // Arrange - const authorId = '1'; - const authorFull = 'somePerson#0001'; + console.log(method) // Act - memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { + return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { expect(result).toEqual(expectedResult); expect(method).toHaveBeenCalledTimes(1); expect(method).toHaveBeenCalledWith(passedIn) From a4804c2ea7cf1e7f86ed2668815b42191cc61e07 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 20:48:44 -0500 Subject: [PATCH 15/59] added babel to convert es modules to cjs --- babel.config.js | 8 ++++++++ tests/jest.config.js => jest.config.js | 12 ++++++------ package.json | 22 +++++----------------- tests/jest.setup.js | 2 -- 4 files changed, 19 insertions(+), 25 deletions(-) create mode 100644 babel.config.js rename tests/jest.config.js => jest.config.js (65%) delete mode 100644 tests/jest.setup.js diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..3c6cd52 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,8 @@ +// babel.config.js +module.exports = { + env: { + test: { + plugins: ["@babel/plugin-transform-modules-commonjs"] + } + } +}; \ No newline at end of file diff --git a/tests/jest.config.js b/jest.config.js similarity index 65% rename from tests/jest.config.js rename to jest.config.js index bdee2b6..cd625d7 100644 --- a/tests/jest.config.js +++ b/jest.config.js @@ -1,11 +1,11 @@ -/** @type {import('jest').Config} */ -const config = { +// jest.config.js +module.exports = { clearMocks: true, collectCoverage: true, coverageDirectory: "coverage", setupFiles: ["/jest.setup.js"], // path to a setup module to configure the testing environment before each test - transform: {}, verbose: true, -}; - -export default config; + transform: { + "^.+\\.[t|j]sx?$": require.resolve('babel-jest') + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index eea7e3a..316602c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "description": "", "main": "src/bot.js", - "type": "module", "repository": { "type": "git", "url": "https://github.com/pieartsy/PluralFlux.git" @@ -19,24 +18,13 @@ "tmp": "^0.2.5" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/preset-env": "^7.29.0", + "babel-jest": "^30.2.0", "jest": "^30.2.0" }, "scripts": { - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" - }, - "jest": { - "testEnvironment": "node", - "coveragePathIgnorePatterns": [ - "/node_modules/" - ], - "moduleFileExtensions": [ - "js", - "json" - ], - "testMatch": [ - "**/__tests__/**/*.js", - "**/?(*.)+(spec|test).js" - ], - "verbose": true + "test": "jest" } } diff --git a/tests/jest.setup.js b/tests/jest.setup.js deleted file mode 100644 index f44af1b..0000000 --- a/tests/jest.setup.js +++ /dev/null @@ -1,2 +0,0 @@ -import * as dotenv from "dotenv"; -dotenv.config({ path: "env.jest" }); From 01e620a935a800b4ea208fd204f464bc43ddba84 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 20:48:54 -0500 Subject: [PATCH 16/59] finally figured out issue with tests (referencing the method directly in the test.each calls the real method not the mock in beforeEach()) --- tests/helpers/memberHelper.test.js | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index f8b44ca..a547fbc 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -33,25 +33,21 @@ describe('MemberHelper', () => { jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("get proxy"); }); - - test.skip.each([ - [['new'], 'new member', memberHelper.addNewMember, [authorId, ['new']]], - [['remove'], 'remove member', memberHelper.removeMember, [authorId, ['remove']]], - [['list'], 'all member info', memberHelper.getAllMembersInfo, [authorId, ['list']]], - [['somePerson', 'name'], 'update name', memberHelper.updateName, [authorId, ['somePerson', 'name']]], - [['somePerson', 'displayname'], 'update display name', memberHelper.updateDisplayName, [authorId, ['somePerson', 'displayname']]], - [['somePerson', 'proxy'], 'get proxy', memberHelper.getProxyByMember, [authorId, 'somePerson']], - [['somePerson', 'proxy', 'test'], 'update proxy', memberHelper.updateProxy, [authorId, ['somePerson', 'proxy', 'test']]], - [['somePerson', 'propic'], 'update propic', memberHelper.updatePropic, [authorId, ['somePerson', 'propic']]], - [['somePerson'], 'member info', getMemberInfoMock, [authorId, 'somePerson']], - ])('%s returns correct values', async (args, expectedResult, method, passedIn) => { - // Arrange - console.log(method) - // Act + test.each([ + [['new'], 'new member', 'addNewMember', ['new']], + [['remove'], 'remove member', 'removeMember', ['remove']], + [['list'], 'all member info', 'getAllMembersInfo', ['list']], + [['somePerson', 'name'], 'update name', 'updateName', ['somePerson', 'name']], + [['somePerson', 'displayname'], 'update display name', 'updateDisplayName', ['somePerson', 'displayname']], + [['somePerson', 'proxy'], 'get proxy', 'getProxyByMember', ['somePerson']], + [['somePerson', 'proxy', 'test'], 'update proxy', 'updateProxy', ['somePerson', 'proxy', 'test']], + [['somePerson', 'propic'], 'update propic', 'updatePropic', ['somePerson', 'propic']], + [['somePerson'], 'member info', 'getMemberInfo', 'somePerson'], + ])('%s returns correct values and calls methods', async (args, expectedResult, method, passedIn) => { return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { expect(result).toEqual(expectedResult); - expect(method).toHaveBeenCalledTimes(1); - expect(method).toHaveBeenCalledWith(passedIn) + expect(memberHelper[method]).toHaveBeenCalledTimes(1); + expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) }); }); From bfc633a7559b6603f6b220740b7f0f4189b3a476 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 20:52:22 -0500 Subject: [PATCH 17/59] setup fixed more --- jest.config.js | 1 - tests/helpers/memberHelper.test.js | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/jest.config.js b/jest.config.js index cd625d7..b0deb3c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,6 @@ module.exports = { clearMocks: true, collectCoverage: true, coverageDirectory: "coverage", - setupFiles: ["/jest.setup.js"], // path to a setup module to configure the testing environment before each test verbose: true, transform: { "^.+\\.[t|j]sx?$": require.resolve('babel-jest') diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index a547fbc..f484e9f 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -1,14 +1,12 @@ -import {jest} from "@jest/globals"; - jest.mock('@fluxerjs/core', () => jest.fn()); jest.mock('../../src/db.js', () => jest.fn()); jest.mock('sequelize', () => jest.fn()); -const {EmbedBuilder} = await import ("@fluxerjs/core"); -const {database} = await import('../../src/db.js'); -const {EmptyResultError, Op} = await import ('sequelize'); -import {enums} from "../../src/enums.js"; -import {memberHelper} from "../../src/helpers/memberHelper.js"; +const {EmbedBuilder} = require("@fluxerjs/core"); +const {database} = require('../../src/db.js'); +const {enums} = require('../../src/enums.js'); +const {EmptyResultError, Op} = require('sequelize'); +const {memberHelper} = require("../../src/helpers/memberHelper.js"); describe('MemberHelper', () => { const authorId = "0001"; From 0b7f549bdf03b3a8b0c9d0c60386f24a5498175c Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 21:20:55 -0500 Subject: [PATCH 18/59] added error handling parseMemberCommand test --- src/helpers/memberHelper.js | 2 +- tests/helpers/memberHelper.test.js | 49 ++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 3bee3cb..c5a9c4c 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -23,7 +23,7 @@ const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', ' mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) { let member; // checks whether command is in list, otherwise assumes it's a name - if (!commandList.includes(args[0])) { + if (!commandList.includes(args[0]) && !args[1]) { member = await mh.getMemberInfo(authorId, args[0]); } switch (args[0]) { diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index f484e9f..2edbbef 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -34,12 +34,11 @@ describe('MemberHelper', () => { test.each([ [['new'], 'new member', 'addNewMember', ['new']], [['remove'], 'remove member', 'removeMember', ['remove']], - [['list'], 'all member info', 'getAllMembersInfo', ['list']], + [['list'], 'all member info', 'getAllMembersInfo', authorFull], [['somePerson', 'name'], 'update name', 'updateName', ['somePerson', 'name']], [['somePerson', 'displayname'], 'update display name', 'updateDisplayName', ['somePerson', 'displayname']], - [['somePerson', 'proxy'], 'get proxy', 'getProxyByMember', ['somePerson']], + [['somePerson', 'proxy'], 'get proxy', 'getProxyByMember', 'somePerson'], [['somePerson', 'proxy', 'test'], 'update proxy', 'updateProxy', ['somePerson', 'proxy', 'test']], - [['somePerson', 'propic'], 'update propic', 'updatePropic', ['somePerson', 'propic']], [['somePerson'], 'member info', 'getMemberInfo', 'somePerson'], ])('%s returns correct values and calls methods', async (args, expectedResult, method, passedIn) => { return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { @@ -49,6 +48,17 @@ describe('MemberHelper', () => { }); }); + test('["somePerson", "propic"] returns correct values and calls methods', () => { + // arrange + const args = ['somePerson', 'propic']; + // act & assert + return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration).then((result) => { + expect(result).toEqual("update propic"); + expect(memberHelper['updatePropic']).toHaveBeenCalledTimes(1); + expect(memberHelper['updatePropic']).toHaveBeenCalledWith(authorId, args, attachmentUrl, attachmentExpiration) + }); + }) + test.each([ [['--help'], enums.help.MEMBER], [['name'], enums.help.NAME], @@ -67,6 +77,39 @@ describe('MemberHelper', () => { }); }); + describe('errors', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + jest.spyOn(memberHelper, 'getMemberInfo').mockImplementation(() => { throw new Error('member info error')}); + jest.spyOn(memberHelper, 'addNewMember').mockImplementation(() => { throw new Error('new member error')}); + jest.spyOn(memberHelper, 'removeMember').mockImplementation(() => { throw new Error('remove member error')}); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockImplementation(() => { throw new Error('all member info error')}); + jest.spyOn(memberHelper, 'updateName').mockImplementation(() => { throw new Error('update name error')}); + jest.spyOn(memberHelper, 'updateDisplayName').mockImplementation(() => { throw new Error('update display name error')}); + jest.spyOn(memberHelper, 'updateProxy').mockImplementation(() => { throw new Error('update proxy error')}); + jest.spyOn(memberHelper, 'updatePropic').mockImplementation(() => { throw new Error('update propic error')}); + jest.spyOn(memberHelper, 'getProxyByMember').mockImplementation(() => { throw new Error('get proxy error')}); + }) + test.each([ + [['new'], 'new member error', 'addNewMember', ['new']], + [['remove'], 'remove member error', 'removeMember', ['remove']], + [['list'], 'all member info error', 'getAllMembersInfo', authorFull], + [['somePerson', 'name'], 'update name error', 'updateName', ['somePerson', 'name']], + [['somePerson', 'displayname'], 'update display name error', 'updateDisplayName', ['somePerson', 'displayname']], + [['somePerson', 'proxy'], 'get proxy error', 'getProxyByMember', 'somePerson'], + [['somePerson', 'proxy', 'test'], 'update proxy error', 'updateProxy', ['somePerson', 'proxy', 'test']], + [['somePerson'], 'member info error', 'getMemberInfo', 'somePerson'], + ])('%s returns correct values and calls methods', async (args, expectedError, method, passedIn) => { + memberHelper.parseMemberCommand(authorId, authorFull, args).catch((result) => { + expect(result).toEqual(new Error(expectedError)); + expect(memberHelper[method]).toHaveBeenCalledTimes(1); + expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) + }); + }); + }) + + afterEach(() => { // restore the spy created with spyOn From c645bb0aeaebb44dda41593bd1428fe13b404984 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 22:23:47 -0500 Subject: [PATCH 19/59] renamed db to database more tests and fixing logic for memberhelper --- src/{db.js => database.js} | 0 src/helpers/memberHelper.js | 46 +++++----- tests/helpers/memberHelper.test.js | 135 ++++++++++++++++++++++++++--- 3 files changed, 149 insertions(+), 32 deletions(-) rename src/{db.js => database.js} (100%) diff --git a/src/db.js b/src/database.js similarity index 100% rename from src/db.js rename to src/database.js diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index c5a9c4c..af31600 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,4 +1,4 @@ -import {database} from '../db.js'; +import {database} from '../database.js'; import {enums} from "../enums.js"; import {EmptyResultError, Op} from "sequelize"; import {EmbedBuilder} from "@fluxerjs/core"; @@ -97,8 +97,8 @@ mh.addNewMember = async function (authorId, args) { const displayName = args[2]; return await mh.addFullMember(authorId, memberName, displayName).then((member) => { - let success = `Member was successfully added.\nName: ${member.dataValues.name}` - success += displayName ? `\nDisplay name: ${member.dataValues.displayname}` : ""; + let success = `Member was successfully added.\nName: ${member.name}` + success += displayName ? `\nDisplay name: ${member.displayname}` : ""; return success; }).catch(e => { throw e; @@ -115,14 +115,17 @@ mh.addNewMember = async function (authorId, args) { * @throws {RangeError} When the name doesn't exist. */ mh.updateName = async function (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.DISPLAY_NAME; + if (args[2] && args[2] === "--help") { + return enums.help.NAME; } const name = args[2]; - const trimmedName = name ? name.trim() : null; - if (!name || trimmedName === null) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + if (!name) { + return `The name for ${name} is ${name}, but you probably knew that!`; + } + const trimmedName = name.trim(); + if (trimmedName === '') { + throw new RangeError(`Name ${enums.err.NO_VALUE}`); } return await mh.updateMemberField(authorId, args).catch((e) => { throw e @@ -147,16 +150,19 @@ mh.updateDisplayName = async function (authorId, args) { const displayName = args[2]; const trimmedName = displayName ? displayName.trim() : null; - if (!displayName || trimmedName === null) { + if (!displayName) { return await mh.getMemberByName(authorId, memberName).then((member) => { if (member && member.displayname) { return `Display name for ${memberName} is: \"${member.displayname}\".`; } else if (member) { - throw new RangeError(`Display name ${enums.err.NO_VALUE}`); + throw new EmptyResultError(`Display name ${enums.err.NO_VALUE}`); } }); } else if (displayName.length > 32) { - throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); + throw new RangeError(enums.err.NO_VALUE); + } + else if (trimmedName === '') { + throw new RangeError(`Display name ${enums.err.NO_VALUE}`); } return await mh.updateMemberField(authorId, args).catch((e) => { throw e @@ -346,15 +352,13 @@ mh.updateMemberField = async function (authorId, args) { name: {[Op.iLike]: memberName}, userid: authorId } - }).then(() => { - return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; - }).catch(e => { - if (e === EmptyResultError) { - throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}: ${e.message}`); - } else { - throw new Error(`Can't update ${memberName}. ${e.message}`); - } - }); + }).then((res) => { + if (res[0] === 0) { + throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`); + } else { + return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; + } + }) } /** @@ -440,7 +444,7 @@ mh.getMemberByName = async function (authorId, memberName) { mh.getProxyByMember = async function (authorId, memberName) { return await mh.getMemberByName(authorId, memberName).then((member) => { if (member) { - return member.dataValues.proxy; + return member.proxy; } throw new EmptyResultError(enums.err.NO_MEMBER); }) diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 2edbbef..aa66628 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -1,9 +1,9 @@ jest.mock('@fluxerjs/core', () => jest.fn()); -jest.mock('../../src/db.js', () => jest.fn()); +jest.mock('../../src/database.js', () => jest.fn()); jest.mock('sequelize', () => jest.fn()); const {EmbedBuilder} = require("@fluxerjs/core"); -const {database} = require('../../src/db.js'); +const {database} = require('../../src/database.js'); const {enums} = require('../../src/enums.js'); const {EmptyResultError, Op} = require('sequelize'); const {memberHelper} = require("../../src/helpers/memberHelper.js"); @@ -14,12 +14,14 @@ describe('MemberHelper', () => { const attachmentUrl = "../oya.png"; const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }) describe('parseMemberCommand', () => { beforeEach(() => { - jest.resetModules(); - jest.clearAllMocks(); jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); @@ -40,8 +42,10 @@ describe('MemberHelper', () => { [['somePerson', 'proxy'], 'get proxy', 'getProxyByMember', 'somePerson'], [['somePerson', 'proxy', 'test'], 'update proxy', 'updateProxy', ['somePerson', 'proxy', 'test']], [['somePerson'], 'member info', 'getMemberInfo', 'somePerson'], - ])('%s returns correct values and calls methods', async (args, expectedResult, method, passedIn) => { + ])('%s calls methods and returns correct values', async (args, expectedResult, method, passedIn) => { + // Act return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { + // Assert expect(result).toEqual(expectedResult); expect(memberHelper[method]).toHaveBeenCalledTimes(1); expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) @@ -49,10 +53,11 @@ describe('MemberHelper', () => { }); test('["somePerson", "propic"] returns correct values and calls methods', () => { - // arrange + // Arrange const args = ['somePerson', 'propic']; - // act & assert + // Act return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration).then((result) => { + // Assert expect(result).toEqual("update propic"); expect(memberHelper['updatePropic']).toHaveBeenCalledTimes(1); expect(memberHelper['updatePropic']).toHaveBeenCalledWith(authorId, args, attachmentUrl, attachmentExpiration) @@ -73,6 +78,7 @@ describe('MemberHelper', () => { const authorFull = 'somePerson#0001'; // Act memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { + expect(result).toEqual(expectedResult); }); }); @@ -100,21 +106,128 @@ describe('MemberHelper', () => { [['somePerson', 'proxy'], 'get proxy error', 'getProxyByMember', 'somePerson'], [['somePerson', 'proxy', 'test'], 'update proxy error', 'updateProxy', ['somePerson', 'proxy', 'test']], [['somePerson'], 'member info error', 'getMemberInfo', 'somePerson'], - ])('%s returns correct values and calls methods', async (args, expectedError, method, passedIn) => { + ])('%s calls methods and throws correct values', async (args, expectedError, method, passedIn) => { + // Act memberHelper.parseMemberCommand(authorId, authorFull, args).catch((result) => { + // Assert expect(result).toEqual(new Error(expectedError)); expect(memberHelper[method]).toHaveBeenCalledTimes(1); expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) }); }); }) + }) + + describe('addNewMember', () => { + + test('returns help if --help passed in', async() => { + // Arrange + const args = ['new', '--help']; + const expected = enums.help.NEW; + //Act + memberHelper.addNewMember(authorId, args).then((result) => { + // Assert + expect(result).toEqual(expected); + }) + }) + + test('returns member without display name when name passed in', async () => { + // Arrange + const args = ['new', 'some person']; + const memberObject = { name: args[1] } + const expected = "Member was successfully added.\nName: " + args[1]; + jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); + //Act + memberHelper.addNewMember(authorId, args).then((result) => { + // Assert + expect(result).toEqual(expected); + }) + }) + + test('returns member with display name when name and display name passed in', async () => { + // Arrange + const args = ['new', 'some person', 'Some person Full Name']; + const memberObject = { name: args[1], displayname: args[2] } + const expected = "Member was successfully added.\nName: " + args[1] + "\nDisplay name: " + args[2]; + jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); + //Act + memberHelper.addNewMember(authorId, args).then((result) => { + // Assert + expect(result).toEqual(expected); + }) + }) + + test('throws expected error when addFullMember throws error', async () => { + // Arrange + const args = ['new', 'somePerson']; + const expected = 'add full member error'; + jest.spyOn(memberHelper, 'addFullMember').mockImplementation(() => { throw new Error(expected)}); + + //Act + memberHelper.addNewMember(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new Error(expected)); + }) + }) + }) + + describe('updateName', () => { + + test('sends help message when --help parameter passed in', async () => { + // Arrange + const args = ['somePerson', 'name', '--help']; + + // Act + memberHelper.updateName(authorId, args).then((result) => { + // Assert + expect(result).toEqual(enums.help.NAME); + }) + }) + test('Sends string when no name', async () => { + // Arrange + const args = ['somePerson', 'name'] + + // Act + memberHelper.updateName(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE)); + }) + }) + test('throws error when name is empty', async () => { + // Arrange + const args = ['somePerson', 'name', " "]; - afterEach(() => { - // restore the spy created with spyOn - jest.restoreAllMocks(); + // Act + memberHelper.updateName(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE)); + }) + }) + + test('throws error when updateMemberField returns error', async () => { + // Arrange + const expected = 'update error'; + const args = ['somePerson', "name", "someNewPerson"]; + jest.spyOn(memberHelper, 'updateMemberField').mockImplementation(() => { + throw new Error(expected) + }); + // Act + memberHelper.updateName(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new Error(expected)); + }) }); }) + + describe('updateDisplayName', () => { + + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); }) From 31eb4262dd9ba657474207ca5b5cbbabca803c70 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 22:23:55 -0500 Subject: [PATCH 20/59] upgraded fluxer.js --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 316602c..c8f1a20 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "private": true, "dependencies": { - "@fluxerjs/core": "^1.0.9", + "@fluxerjs/core": "^1.1.5", "dotenv": "^17.3.1", "pg": "^8.18.0", "pg-hstore": "^2.3.4", From 3dbbe7df50f453c3a03f74529a9f6add9be3451d Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 23:03:08 -0500 Subject: [PATCH 21/59] moved import to helpers folder --- src/{import.js => helpers/importHelper.js} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/{import.js => helpers/importHelper.js} (95%) diff --git a/src/import.js b/src/helpers/importHelper.js similarity index 95% rename from src/import.js rename to src/helpers/importHelper.js index feee479..cb520ce 100644 --- a/src/import.js +++ b/src/helpers/importHelper.js @@ -1,5 +1,5 @@ -import {enums} from "./enums.js"; -import {memberHelper} from "./helpers/memberHelper.js"; +import {enums} from "../enums.js"; +import {memberHelper} from "./memberHelper.js"; const ih = {}; From 15703c24cdb688acacf41a9b74c5ba09eed1a31f Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 23:03:19 -0500 Subject: [PATCH 22/59] moved import to helpers folder --- src/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.js b/src/commands.js index 83230c7..893e203 100644 --- a/src/commands.js +++ b/src/commands.js @@ -2,7 +2,7 @@ import {messageHelper} from "./helpers/messageHelper.js"; import {enums} from "./enums.js"; import {memberHelper} from "./helpers/memberHelper.js"; import {EmbedBuilder} from "@fluxerjs/core"; -import {importHelper} from "./import.js"; +import {importHelper} from "./helpers/importHelper.js"; const cmds = new Map(); From fe00f6610448ab55ece7181172b2aeba9a9b9f86 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Tue, 17 Feb 2026 23:03:45 -0500 Subject: [PATCH 23/59] more tests for member helper --- tests/helpers/memberHelper.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index aa66628..53ba7ee 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -223,6 +223,32 @@ describe('MemberHelper', () => { describe('updateDisplayName', () => { + test('sends help message when --help parameter passed in', async () => { + // Arrange + const args = ['somePerson', 'displayname', '--help']; + + // Act + memberHelper.updateDisplayName(authorId, args).then((result) => { + // Assert + expect(result).toEqual(enums.help.DISPLAY_NAME); + }) + }) + + test('Sends string of current displayname when no displayname passed in', async () => { + // Arrange + const args = ['somePerson', 'displayname']; + const displayname = "Some Person"; + const member = { + displayname: displayname, + } + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); + + // Act + memberHelper.updateDisplayName(authorId, args).then((result) => { + // Assert + expect(result).toEqual(`Display name for ${args[0]} is: "${member.displayname}".`); + }) + }) }) afterEach(() => { From 1bf6c8c1f2109ef06ae874ce9c9e5273cbfaf2a6 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 00:25:23 -0500 Subject: [PATCH 24/59] think i fixed weird error with webhook sending error when a user has no members --- src/helpers/webhookHelper.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index dd78184..077404f 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -16,18 +16,14 @@ wh.sendMessageAsMember = async function(client, message) { const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e}); // If the message doesn't match a proxy, just return. - if (!proxyMatch.member) { + if (!proxyMatch || !proxyMatch.member) { return; } // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs if (!message.guildId) { throw new Error(enums.err.NOT_IN_SERVER); } - - if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { - return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname}`) - } - await replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); + await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); } /** @@ -38,10 +34,10 @@ wh.sendMessageAsMember = async function(client, message) { * @param {model} member - A member object from the database. * @throws {Error} When there's no message to send. */ -async function replaceMessage(client, message, text, member) { +wh.replaceMessage = async function(client, message, text, member) { if (text.length > 0 || message.attachments.size > 0) { const channel = client.channels.get(message.channelId); - const webhook = await getOrCreateWebhook(client, channel).catch((e) =>{throw e}); + const webhook = await wh.getOrCreateWebhook(client, channel).catch((e) =>{throw e}); const username = member.displayname ?? member.name; await webhook.send({content: text, username: username, avatar_url: member.propic}); await message.delete(); @@ -59,10 +55,10 @@ async function replaceMessage(client, message, text, member) { * @returns {Webhook} A webhook object. * @throws {Error} When no webhooks are allowed in the channel. */ -async function getOrCreateWebhook(client, channel) { +wh.getOrCreateWebhook = async function(client, channel) { // If channel doesn't allow webhooks if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED); - let webhook = await getWebhook(client, channel).catch((e) =>{throw e}); + let webhook = await wh.getWebhook(client, channel).catch((e) =>{throw e}); if (!webhook) { webhook = await channel.createWebhook({name: name}); } @@ -76,7 +72,7 @@ async function getOrCreateWebhook(client, channel) { * @param {Channel} channel - The channel the message was sent in. * @returns {Webhook} A webhook object. */ -async function getWebhook(client, channel) { +wh.getWebhook = async function(client, channel) { const channelWebhooks = await channel?.fetchWebhooks() ?? []; if (channelWebhooks.length === 0) { return; From 23a57b3e99e1c1fcfc1b9f9f8c5fdddf08dac1f3 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 00:26:43 -0500 Subject: [PATCH 25/59] simplified sendMessageAsAttachment --- src/helpers/messageHelper.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index 94a200a..1c0777a 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -72,23 +72,17 @@ msgh.parseProxyTags = async function (authorId, content, attachmentUrl= null){ } /** - * Sends a message as an attachment if it's too long.NOT CURRENTLY IN USE + * Sends a message as an attachment if it's too long. * * @async * @param {string} text - The text of the message. * @param {Message} message - The message object. - * @throws {Error} If a proxy message is sent with no message within it. * */ -msgh.sendMessageAsAttachment = async function(text, message) { +msgh.sendMessageAsAttachment = async function (text, message) { if (text.length > 2000) { - tmp.file(async (err, path, fd, cleanupCallback) => { - fs.writeFile(path, text, (err) => { - if (err) throw err; - }) - if (err) throw err; - await message.reply({content: enums.err.IMPORT_ERROR, attachments: [path]}); - }); + const data = Buffer.from(text, 'utf-8'); + await message.reply({content: enums.err.IMPORT_ERROR, files: [{name: 'import-logs.txt', data}]}); } } From da5a250445df32f67daa926ac53291e6ac3f2096 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 00:27:33 -0500 Subject: [PATCH 26/59] added return to addFullMember so that addNewMember can reference it properly in strings --- src/helpers/memberHelper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index af31600..430efae 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -326,6 +326,7 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro if (!member) { new Error(`${enums.err.ADD_ERROR}`); } + return member; } /** From 5c01f2e2845c8b8b26f9f6e0fc6faf93825691af Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 00:28:18 -0500 Subject: [PATCH 27/59] test setup for messagehelper and webhookhelper --- tests/helpers/messageHelper.test.js | 33 +++++++++++++++++++++++++++++ tests/helpers/webhookHelper.test.js | 27 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/helpers/messageHelper.test.js create mode 100644 tests/helpers/webhookHelper.test.js diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js new file mode 100644 index 0000000..64b8917 --- /dev/null +++ b/tests/helpers/messageHelper.test.js @@ -0,0 +1,33 @@ +jest.mock('../../src/helpers/memberHelper.js') +jest.mock('tmp'); +jest.mock('fs'); +jest.mock('@fluxerjs/core', () => jest.fn()); + +const {memberHelper} = require("../../src/helpers/memberHelper.js"); +const {Message} = require("@fluxerjs/core"); +const {fs} = require('fs'); +const {enums} = require('../../src/enums'); +const {tmp, setGracefulCleanup} = require('tmp'); +const {messageHelper} = require("../../src/helpers/memberHelper.js"); +const {describe} = require("pm2"); + +describe('messageHelper', () => { + const authorId = "0001"; + const authorFull = "author#0001"; + const attachmentUrl = "../oya.png"; + const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }) + + describe(`parseProxyTags`, () => { + + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) \ No newline at end of file diff --git a/tests/helpers/webhookHelper.test.js b/tests/helpers/webhookHelper.test.js new file mode 100644 index 0000000..47f85d4 --- /dev/null +++ b/tests/helpers/webhookHelper.test.js @@ -0,0 +1,27 @@ +jest.mock('../../src/helpers/messageHelper.js') +jest.mock('@fluxerjs/core', () => jest.fn()); + +const {messageHelper} = require("../../src/helpers/messageHelper.js"); +const {Message, Webhook, Channel} = require("@fluxerjs/core"); +const {webhookHelper} = require("../../src/helpers/webhookHelper.js"); + +describe('webhookHelper', () => { + const authorId = "0001"; + const authorFull = "author#0001"; + const attachmentUrl = "../oya.png"; + const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }) + + describe(`sendMessageAsMember`, () => { + + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) \ No newline at end of file From f0ac02e86d47b507d24a4fe37ce96d000ca91dfc Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 00:33:05 -0500 Subject: [PATCH 28/59] readded line i shouldn't have removed in sendMessageAsMember --- src/helpers/webhookHelper.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index 077404f..ca5055e 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -23,7 +23,10 @@ wh.sendMessageAsMember = async function(client, message) { if (!message.guildId) { throw new Error(enums.err.NOT_IN_SERVER); } - await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); + if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { + return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname ?? proxyMatch.member.name}`) + } + await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); } /** From e16694ac2df5d88dbeb9da762481dc43b001f494 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 08:47:37 -0500 Subject: [PATCH 29/59] fixed test and logic --- src/helpers/memberHelper.js | 2 +- tests/helpers/memberHelper.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 430efae..721ece0 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -121,7 +121,7 @@ mh.updateName = async function (authorId, args) { const name = args[2]; if (!name) { - return `The name for ${name} is ${name}, but you probably knew that!`; + return `The name for ${args[0]} is ${args[0]}, but you probably knew that!`; } const trimmedName = name.trim(); if (trimmedName === '') { diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 53ba7ee..21d7c9f 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -186,12 +186,12 @@ describe('MemberHelper', () => { test('Sends string when no name', async () => { // Arrange - const args = ['somePerson', 'name'] + const args = ['somePerson', 'name']; + const expected = `The name for ${args[0]} is ${args[0]}, but you probably knew that!`; // Act - memberHelper.updateName(authorId, args).catch((result) => { - // Assert - expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE)); + memberHelper.updateName(authorId, args).then((result) => { + expect(result).toEqual(expected); }) }) From 152bc8873d087326a633325b54136cca051cd5f7 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 09:16:26 -0500 Subject: [PATCH 30/59] added test for memberHelper --- tests/helpers/memberHelper.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 21d7c9f..a24bec2 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -219,6 +219,18 @@ describe('MemberHelper', () => { expect(result).toEqual(new Error(expected)); }) }); + + test('sends string when updateMemberField returns successfully', async () => { + // Arrange + const args = ['somePerson', 'name', 'someNewPerson']; + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); + + // Act + memberHelper.updateName(authorId, args).then((result) => { + // Assert + expect(result).toEqual("Updated"); + }) + }) }) describe('updateDisplayName', () => { From 400e40a40588f366433a81336a716928ddb07a60 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 09:19:44 -0500 Subject: [PATCH 31/59] updated sendMessageAsAttachment to returnBufferFromText and updated commands/webhookHelper accordingly --- src/commands.js | 4 +++- src/helpers/messageHelper.js | 14 ++++++++------ src/helpers/webhookHelper.js | 7 ++++++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/commands.js b/src/commands.js index 893e203..469314c 100644 --- a/src/commands.js +++ b/src/commands.js @@ -59,7 +59,9 @@ cmds.set('import', { let errorsText = `${error.message}.\nThese errors occurred:\n${error.errors.join('\n')}`; await message.reply(errorsText).catch(async () => { - await messageHelper.sendMessageAsAttachment(errorsText, message); + const returnedBuffer = await messageHelper.returnBufferFromText(errorsText); + await message.reply({content: returnedBuffer.text, files: [{ name: 'text.pdf', data: returnedBuffer.file }] + }) }); } // If just one error was returned. diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index 1c0777a..66296f5 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -44,7 +44,7 @@ msgh.parseCommandArgs = function(content, commandName) { * @returns {Object} The proxy message object. * @throws {Error} If a proxy message is sent with no message within it. */ -msgh.parseProxyTags = async function (authorId, content, attachmentUrl= null){ +msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ const members = await memberHelper.getMembersByAuthor(authorId); // If an author has no members, no sense in searching for proxy if (members.length === 0) { @@ -72,17 +72,19 @@ msgh.parseProxyTags = async function (authorId, content, attachmentUrl= null){ } /** - * Sends a message as an attachment if it's too long. + * Returns a text message that's too long as its text plus a file with the remaining text. * * @async * @param {string} text - The text of the message. - * @param {Message} message - The message object. + * @returns {Object} The text and buffer object * */ -msgh.sendMessageAsAttachment = async function (text, message) { +msgh.returnBufferFromText = async function (text) { if (text.length > 2000) { - const data = Buffer.from(text, 'utf-8'); - await message.reply({content: enums.err.IMPORT_ERROR, files: [{name: 'import-logs.txt', data}]}); + const truncated = text.substring(0, 2000); + const restOfText = text.substring(2001); + const file = Buffer.from(restOfText, 'utf-8'); + return {text: truncated, file: file} } } diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index ca5055e..c74a3e3 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -42,7 +42,12 @@ wh.replaceMessage = async function(client, message, text, member) { const channel = client.channels.get(message.channelId); const webhook = await wh.getOrCreateWebhook(client, channel).catch((e) =>{throw e}); const username = member.displayname ?? member.name; - await webhook.send({content: text, username: username, avatar_url: member.propic}); + await webhook.send({content: text, username: username, avatar_url: member.propic}).catch(async(e) => { + const returnedBuffer = await messageHelper.returnBufferFromText(text); + await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.pdf', data: returnedBuffer.file }] + }) + console.error(e); + }); await message.delete(); } else { From 223292c2d39b27108d1552882223ce4b6e981baa Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 10:16:51 -0500 Subject: [PATCH 32/59] added tests for parseProxyTags and updated logic --- src/helpers/messageHelper.js | 4 +- tests/helpers/messageHelper.test.js | 66 ++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index 66296f5..037040f 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -57,14 +57,14 @@ msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ const splitProxy = member.proxy.split("text"); if(content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { proxyMessage.member = member; - if (attachmentUrl) return proxyMessage.message = enums.misc.ATTACHMENT_SENT_BY; + if (attachmentUrl) proxyMessage.hasAttachment = true; let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); escapedPrefix = new RegExp("^" + escapedPrefix); escapedSuffix = new RegExp(escapedSuffix + "$") proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); - if (proxyMessage.message.length === 0) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); + if (proxyMessage.message.length === 0 && !attachmentUrl) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); } } }) diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index 64b8917..aa32234 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -1,17 +1,26 @@ -jest.mock('../../src/helpers/memberHelper.js') -jest.mock('tmp'); -jest.mock('fs'); -jest.mock('@fluxerjs/core', () => jest.fn()); +const env = require('dotenv'); +env.config(); const {memberHelper} = require("../../src/helpers/memberHelper.js"); const {Message} = require("@fluxerjs/core"); const {fs} = require('fs'); const {enums} = require('../../src/enums'); const {tmp, setGracefulCleanup} = require('tmp'); -const {messageHelper} = require("../../src/helpers/memberHelper.js"); -const {describe} = require("pm2"); + +jest.mock('../../src/helpers/memberHelper.js', () => { + return {memberHelper: { + getMembersByAuthor: jest.fn() + }} +}) + +jest.mock('tmp'); +jest.mock('fs'); +jest.mock('@fluxerjs/core'); + +const {messageHelper} = require("../../src/helpers/messageHelper.js"); describe('messageHelper', () => { + // let memberHelper = {} const authorId = "0001"; const authorFull = "author#0001"; const attachmentUrl = "../oya.png"; @@ -23,6 +32,51 @@ describe('messageHelper', () => { }) describe(`parseProxyTags`, () => { + const membersFor1 = [ + {name: "somePerson", proxy: "--text"}, + {name: "someSecondPerson", proxy: undefined} + ] + + const membersFor2 = [] + + const membersFor3 = [ + {name: "someOtherThirdPerson", proxy: undefined} + ] + + const attachmentUrl = "../oya.png" + + beforeEach(() => { + memberHelper.getMembersByAuthor = jest.fn().mockImplementation((specificAuthorId) => { + if (specificAuthorId === "1") return membersFor1; + if (specificAuthorId === "2") return membersFor2; + if (specificAuthorId === "3") return membersFor3; + }) + }); + + test.each([ + ['1', 'hello', null, {}], + ['1', '--hello', null, {member: membersFor1[0], message: 'hello'}], + ['1', 'hello', attachmentUrl, {}], + ['1', '--hello', attachmentUrl, {member: membersFor1[0], message: 'hello', hasAttachment: true}], + ['1', '--', attachmentUrl, {member: membersFor1[0], message: '', hasAttachment: true}], + ['2', 'hello', null, undefined], + ['2', '--hello', null, undefined], + ['2', 'hello', attachmentUrl, undefined], + ['2', '--hello', attachmentUrl,undefined], + ['3', 'hello', null, {}], + ['3', '--hello', null, {}], + ['3', 'hello', attachmentUrl, {}], + ['3', '--hello', attachmentUrl,{}], + ])('Member %s returns correct proxy', (specificAuthorId, content, attachmentUrl, expected) => { + messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => { + expect(res).toEqual(expected); + }) + }); + + + }) + + describe('parseCommandArgs', () => { }) From 274f1ead1525a1fb6e9617f6762d5ff414903a4e Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 12:11:55 -0500 Subject: [PATCH 33/59] added "return" so tests dont terminate on failure and deleted env.jest --- tests/env.jest | 2 -- tests/helpers/memberHelper.test.js | 26 +++++++++++++------------- tests/helpers/messageHelper.test.js | 9 ++++++++- 3 files changed, 21 insertions(+), 16 deletions(-) delete mode 100644 tests/env.jest diff --git a/tests/env.jest b/tests/env.jest deleted file mode 100644 index c130787..0000000 --- a/tests/env.jest +++ /dev/null @@ -1,2 +0,0 @@ -FLUXER_BOT_TOKEN=jest-fluxer-bot-token -POSTGRES_PASSWORD=jest-postgres-password \ No newline at end of file diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index a24bec2..3b6ff43 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -77,7 +77,7 @@ describe('MemberHelper', () => { const authorId = '1'; const authorFull = 'somePerson#0001'; // Act - memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { + return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { expect(result).toEqual(expectedResult); }); @@ -108,7 +108,7 @@ describe('MemberHelper', () => { [['somePerson'], 'member info error', 'getMemberInfo', 'somePerson'], ])('%s calls methods and throws correct values', async (args, expectedError, method, passedIn) => { // Act - memberHelper.parseMemberCommand(authorId, authorFull, args).catch((result) => { + return memberHelper.parseMemberCommand(authorId, authorFull, args).catch((result) => { // Assert expect(result).toEqual(new Error(expectedError)); expect(memberHelper[method]).toHaveBeenCalledTimes(1); @@ -125,7 +125,7 @@ describe('MemberHelper', () => { const args = ['new', '--help']; const expected = enums.help.NEW; //Act - memberHelper.addNewMember(authorId, args).then((result) => { + return memberHelper.addNewMember(authorId, args).then((result) => { // Assert expect(result).toEqual(expected); }) @@ -138,7 +138,7 @@ describe('MemberHelper', () => { const expected = "Member was successfully added.\nName: " + args[1]; jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); //Act - memberHelper.addNewMember(authorId, args).then((result) => { + return memberHelper.addNewMember(authorId, args).then((result) => { // Assert expect(result).toEqual(expected); }) @@ -151,7 +151,7 @@ describe('MemberHelper', () => { const expected = "Member was successfully added.\nName: " + args[1] + "\nDisplay name: " + args[2]; jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); //Act - memberHelper.addNewMember(authorId, args).then((result) => { + return memberHelper.addNewMember(authorId, args).then((result) => { // Assert expect(result).toEqual(expected); }) @@ -164,7 +164,7 @@ describe('MemberHelper', () => { jest.spyOn(memberHelper, 'addFullMember').mockImplementation(() => { throw new Error(expected)}); //Act - memberHelper.addNewMember(authorId, args).catch((result) => { + return memberHelper.addNewMember(authorId, args).catch((result) => { // Assert expect(result).toEqual(new Error(expected)); }) @@ -178,7 +178,7 @@ describe('MemberHelper', () => { const args = ['somePerson', 'name', '--help']; // Act - memberHelper.updateName(authorId, args).then((result) => { + return memberHelper.updateName(authorId, args).then((result) => { // Assert expect(result).toEqual(enums.help.NAME); }) @@ -190,7 +190,7 @@ describe('MemberHelper', () => { const expected = `The name for ${args[0]} is ${args[0]}, but you probably knew that!`; // Act - memberHelper.updateName(authorId, args).then((result) => { + return memberHelper.updateName(authorId, args).then((result) => { expect(result).toEqual(expected); }) }) @@ -200,7 +200,7 @@ describe('MemberHelper', () => { const args = ['somePerson', 'name', " "]; // Act - memberHelper.updateName(authorId, args).catch((result) => { + return memberHelper.updateName(authorId, args).catch((result) => { // Assert expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE)); }) @@ -214,7 +214,7 @@ describe('MemberHelper', () => { throw new Error(expected) }); // Act - memberHelper.updateName(authorId, args).catch((result) => { + return memberHelper.updateName(authorId, args).catch((result) => { // Assert expect(result).toEqual(new Error(expected)); }) @@ -226,7 +226,7 @@ describe('MemberHelper', () => { jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); // Act - memberHelper.updateName(authorId, args).then((result) => { + return memberHelper.updateName(authorId, args).then((result) => { // Assert expect(result).toEqual("Updated"); }) @@ -240,7 +240,7 @@ describe('MemberHelper', () => { const args = ['somePerson', 'displayname', '--help']; // Act - memberHelper.updateDisplayName(authorId, args).then((result) => { + return memberHelper.updateDisplayName(authorId, args).then((result) => { // Assert expect(result).toEqual(enums.help.DISPLAY_NAME); }) @@ -256,7 +256,7 @@ describe('MemberHelper', () => { jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); // Act - memberHelper.updateDisplayName(authorId, args).then((result) => { + return memberHelper.updateDisplayName(authorId, args).then((result) => { // Assert expect(result).toEqual(`Display name for ${args[0]} is: "${member.displayname}".`); }) diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index aa32234..430e07a 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -68,11 +68,18 @@ describe('messageHelper', () => { ['3', 'hello', attachmentUrl, {}], ['3', '--hello', attachmentUrl,{}], ])('Member %s returns correct proxy', (specificAuthorId, content, attachmentUrl, expected) => { - messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => { + return messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => { expect(res).toEqual(expected); }) }); + test('expect error to be thrown when no message is present', () => { + // Arrange + return messageHelper.parseProxyTags('1', '', null).catch((res) => { + expect(res).toEqual(new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY)); + }) + }) + }) From da9a3d2c8a8d5a12627f3db6541e1ab64b65d4f6 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 12:37:39 -0500 Subject: [PATCH 34/59] finished tests for messageHelper! --- src/helpers/messageHelper.js | 10 +++--- tests/helpers/messageHelper.test.js | 50 ++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index 037040f..f8771e1 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -21,7 +21,7 @@ msgh.parseCommandArgs = function(content, commandName) { const message = content.slice(msgh.prefix.length + commandName.length).trim(); return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { - if (chara === '"') { + if (chara === '\"' || chara === '\'') { // checks whether string is within quotes or not accumulator.quote ^= 1; } else if (!accumulator.quote && chara === ' '){ @@ -74,18 +74,18 @@ msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ /** * Returns a text message that's too long as its text plus a file with the remaining text. * - * @async * @param {string} text - The text of the message. - * @returns {Object} The text and buffer object + * @returns {{text: string, file: Buffer | undefined}} The text and buffer object * */ -msgh.returnBufferFromText = async function (text) { +msgh.returnBufferFromText = function (text) { if (text.length > 2000) { const truncated = text.substring(0, 2000); - const restOfText = text.substring(2001); + const restOfText = text.substring(2000); const file = Buffer.from(restOfText, 'utf-8'); return {text: truncated, file: file} } + return {text: text, file: undefined} } export const messageHelper = msgh; diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index 430e07a..67ffbf5 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -20,17 +20,27 @@ jest.mock('@fluxerjs/core'); const {messageHelper} = require("../../src/helpers/messageHelper.js"); describe('messageHelper', () => { - // let memberHelper = {} - const authorId = "0001"; - const authorFull = "author#0001"; - const attachmentUrl = "../oya.png"; - const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); }) + describe('parseCommandArgs', () => { + test.each([ + ['pk;member', ['']], + ['pk;member add somePerson "Some Person"', ['add', 'somePerson', 'Some Person']], + ['pk;member add \"Some Person\"', ['add', 'Some Person']], + ['pk;member add somePerson \'Some Person\'', ['add', 'somePerson', 'Some Person']], + ['pk;member add somePerson \"\'Some\' Person\"', ['add', 'somePerson', 'Some Person']], + ])('%s returns correct arguments', (content, expected) => { + // Arrange + const command = "member"; + const result = messageHelper.parseCommandArgs(content, command); + expect(result).toEqual(expected); + }) + }) + describe(`parseProxyTags`, () => { const membersFor1 = [ {name: "somePerson", proxy: "--text"}, @@ -68,23 +78,47 @@ describe('messageHelper', () => { ['3', 'hello', attachmentUrl, {}], ['3', '--hello', attachmentUrl,{}], ])('Member %s returns correct proxy', (specificAuthorId, content, attachmentUrl, expected) => { + // Act return messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => { + // Assert expect(res).toEqual(expected); }) }); test('expect error to be thrown when no message is present', () => { - // Arrange + // Act return messageHelper.parseProxyTags('1', '', null).catch((res) => { + // Assert expect(res).toEqual(new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY)); }) }) + }) + describe('returnBufferFromText', () => { + const charas2000 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - }) + test('returns truncated text and buffer file when text is more than 2000 characters', () => { + // Arrange - describe('parseCommandArgs', () => { + const charasOver2000 = "bbbbb" + const expectedBuffer = Buffer.from(charasOver2000, 'utf-8'); + const expected = {text: charas2000, file: expectedBuffer}; + + // Act + const result = messageHelper.returnBufferFromText(`${charas2000}${charasOver2000}`); + // Assert + expect(result).toEqual(expected); + }) + + test('returns text when text is 2000 characters or less', () => { + // Arrange + const expected = {text: charas2000, file: undefined}; + // Act + const result = messageHelper.returnBufferFromText(`${charas2000}`); + // Assert + expect(result).toEqual(expected); + }) }) afterEach(() => { From acd9ce7c3e9f068407295e09addd5ef6000d2749 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 16:28:35 -0500 Subject: [PATCH 35/59] more cases for messageHelper just in case --- tests/helpers/messageHelper.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index 67ffbf5..aff235d 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -44,7 +44,10 @@ describe('messageHelper', () => { describe(`parseProxyTags`, () => { const membersFor1 = [ {name: "somePerson", proxy: "--text"}, - {name: "someSecondPerson", proxy: undefined} + {name: "someSecondPerson", proxy: undefined}, +, {name: "someOtherPerson", proxy: "?text}"}, + {name: "someLastPerson", proxy: "{text}"}, + {name: "someEmojiPerson", proxy: "⭐text"}, ] const membersFor2 = [] @@ -69,6 +72,9 @@ describe('messageHelper', () => { ['1', 'hello', attachmentUrl, {}], ['1', '--hello', attachmentUrl, {member: membersFor1[0], message: 'hello', hasAttachment: true}], ['1', '--', attachmentUrl, {member: membersFor1[0], message: '', hasAttachment: true}], + ['1', '?hello}', null, {member: membersFor1[3], message: 'hello'}], + ['1', '{hello}', null, {member: membersFor1[4], message: 'hello'}], + ['1', '⭐hello', null, {member: membersFor1[5], message: 'hello'}], ['2', 'hello', null, undefined], ['2', '--hello', null, undefined], ['2', 'hello', attachmentUrl, undefined], @@ -77,7 +83,7 @@ describe('messageHelper', () => { ['3', '--hello', null, {}], ['3', 'hello', attachmentUrl, {}], ['3', '--hello', attachmentUrl,{}], - ])('Member %s returns correct proxy', (specificAuthorId, content, attachmentUrl, expected) => { + ])('ID %s with string %s returns correct proxy', async(specificAuthorId, content, attachmentUrl, expected) => { // Act return messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => { // Assert From 1bba8099e96a8a3d34186c43d664abebbe27ebf2 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 16:29:08 -0500 Subject: [PATCH 36/59] updating docstring for messageHelper parseProxyTags --- src/helpers/messageHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index f8771e1..ac07535 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -41,7 +41,7 @@ msgh.parseCommandArgs = function(content, commandName) { * @param {string} authorId - The author of the message. * @param {string} content - The full message content * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. - * @returns {Object} The proxy message object. + * @returns {{model, string, bool}} The proxy message object. * @throws {Error} If a proxy message is sent with no message within it. */ msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ From fc1c4636964cacaaecce8328309a38eeff5813be Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 17:14:04 -0500 Subject: [PATCH 37/59] more tests for webhookhelper --- src/helpers/webhookHelper.js | 10 +-- tests/helpers/webhookHelper.test.js | 123 ++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index c74a3e3..9aea380 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -1,5 +1,5 @@ import {messageHelper} from "./messageHelper.js"; -import {Webhook, Channel, Message} from '@fluxerjs/core'; +import {Webhook, Channel, Message, Client} from '@fluxerjs/core'; import {enums} from "../enums.js"; const wh = {}; @@ -19,14 +19,14 @@ wh.sendMessageAsMember = async function(client, message) { if (!proxyMatch || !proxyMatch.member) { return; } - // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs + // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs) if (!message.guildId) { throw new Error(enums.err.NOT_IN_SERVER); } - if (proxyMatch.message === enums.misc.ATTACHMENT_SENT_BY) { + if (proxyMatch.hasAttachment) { return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname ?? proxyMatch.member.name}`) } - await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); + await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e}); } /** @@ -43,7 +43,7 @@ wh.replaceMessage = async function(client, message, text, member) { const webhook = await wh.getOrCreateWebhook(client, channel).catch((e) =>{throw e}); const username = member.displayname ?? member.name; await webhook.send({content: text, username: username, avatar_url: member.propic}).catch(async(e) => { - const returnedBuffer = await messageHelper.returnBufferFromText(text); + const returnedBuffer = messageHelper.returnBufferFromText(text); await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.pdf', data: returnedBuffer.file }] }) console.error(e); diff --git a/tests/helpers/webhookHelper.test.js b/tests/helpers/webhookHelper.test.js index 47f85d4..2b5a2be 100644 --- a/tests/helpers/webhookHelper.test.js +++ b/tests/helpers/webhookHelper.test.js @@ -1,15 +1,23 @@ jest.mock('../../src/helpers/messageHelper.js') -jest.mock('@fluxerjs/core', () => jest.fn()); const {messageHelper} = require("../../src/helpers/messageHelper.js"); -const {Message, Webhook, Channel} = require("@fluxerjs/core"); +const {Message, Webhook, Channel, Client} = require("@fluxerjs/core"); + +jest.mock('../../src/helpers/messageHelper.js', () => { + return {messageHelper: { + parseProxyTags: jest.fn(), + returnBuffer: jest.fn() + }} +}) + +// jest.mock("@fluxerjs/core"); + const {webhookHelper} = require("../../src/helpers/webhookHelper.js"); +const {enums} = require("../../src/enums"); describe('webhookHelper', () => { - const authorId = "0001"; - const authorFull = "author#0001"; - const attachmentUrl = "../oya.png"; - const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + + const client = new Client(); beforeEach(() => { jest.resetModules(); @@ -17,9 +25,112 @@ describe('webhookHelper', () => { }) describe(`sendMessageAsMember`, () => { + const content = "hi" + const attachments = new Map(); + const message = { + client, + content: content, + attachments: attachments, + author: { + id: '123' + }, + guild: { + guildId: '123' + }, + reply: jest.fn() + } + const member = {proxy: "--text", name: 'somePerson', displayname: "Some Person"}; + const proxyMessage = {message: content, member: member} + beforeEach(() => { + jest.spyOn(webhookHelper, 'replaceMessage'); + + }) + + test('calls parseProxyTags and returns if proxyMatch is empty object', async() => { + // Arrange + messageHelper.parseProxyTags.mockResolvedValue({}); + // Act + return webhookHelper.sendMessageAsMember(client, message).then((res) => { + expect(res).toBeUndefined(); + expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1); + expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null); + expect(webhookHelper.replaceMessage).not.toHaveBeenCalled(); + }) + }) + + test('calls parseProxyTags and returns if proxyMatch is undefined', async() => { + // Arrange + messageHelper.parseProxyTags.mockResolvedValue(undefined); + // Act + return webhookHelper.sendMessageAsMember(client, message).then((res) => { + // Assert + expect(res).toBeUndefined(); + expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1); + expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null); + }) + }) + + test('calls parseProxyTags with attachmentUrl', async() => { + // Arrange + message.attachments = { + size: 1, + first: () => { + return {url: 'oya.png'} + } + } + // message.attachments.set('attachment', {url: 'oya.png'}) + // message.attachments.set('first', () => {return {url: 'oya.png'}}) + messageHelper.parseProxyTags.mockResolvedValue(undefined); + // Act + return webhookHelper.sendMessageAsMember(client, message).then((res) => { + // Assert + expect(res).toBeUndefined(); + expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1); + expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, 'oya.png'); + }) + }) + + test('if message matches member proxy but is not sent from a guild, throw an error', async() => { + // Arrange + messageHelper.parseProxyTags.mockResolvedValue(proxyMessage); + // Act + return webhookHelper.sendMessageAsMember(client, message).catch((res) => { + // Assert + expect(res).toEqual(new Error(enums.err.NOT_IN_SERVER)); + }) + }) + + test('if message matches member proxy and sent in a guild and has an attachment, reply to message with ping', async() => { + // Arrange + message.guildId = '123' + proxyMessage.hasAttachment = true; + messageHelper.parseProxyTags.mockResolvedValue(proxyMessage); + const expected = `${enums.misc.ATTACHMENT_SENT_BY} ${proxyMessage.member.displayname}` + // Act + return webhookHelper.sendMessageAsMember(client, message).then((res) => { + // Assert + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith(expected); + expect(webhookHelper.replaceMessage).not.toHaveBeenCalled(); + }) + }) + }) + describe(`replaceMessage`, () => { + + }) + + describe(`getOrCreateWebhook`, () => { + + }) + + describe(`getWebhook`, () => { + + }) + + afterEach(() => { // restore the spy created with spyOn jest.restoreAllMocks(); From 75c4c548d86479b32d0019460466bc5962f78044 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 21:13:25 -0500 Subject: [PATCH 38/59] deleted extra file added during merge --- tests/helpers/membersHelper.test.js | 59 ----------------------------- 1 file changed, 59 deletions(-) delete mode 100644 tests/helpers/membersHelper.test.js diff --git a/tests/helpers/membersHelper.test.js b/tests/helpers/membersHelper.test.js deleted file mode 100644 index b3ee50c..0000000 --- a/tests/helpers/membersHelper.test.js +++ /dev/null @@ -1,59 +0,0 @@ -jest.mock('@fluxerjs/core', () => jest.fn()); -jest.mock('../../src/db.js', () => jest.fn()); -jest.mock('sequelize', () => jest.fn()); -jest.mock('../../src/enums.js', () => ({ - enums: jest.requireActual('../../src/enums.js') -})); - -const {enums} = require("../../src/enums.js"); -const memberHelper = require("../../src/helpers/memberHelper.js"); - -describe('parseMemberCommand', () => { - - beforeEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); - jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); - jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); - jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); - jest.spyOn(memberHelper, 'updateName').mockResolvedValue("update name"); - jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); - jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); - jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); - jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("update proxy"); - }); - - - test.each([ - [['--help'], enums.help.MEMBER], - [['new'], 'new member'], - [['remove'], 'remove member'], - [['name'], enums.help.NAME], - [['displayname'], enums.help.DISPLAY_NAME], - [['proxy'], enums.help.PROXY], - [['propic'], enums.help.PROPIC], - [['list'], 'all member info'], - [[''], enums.help.MEMBER], - [['somePerson', 'new'], enums.help.NEW], - [['somePerson', 'name'], 'update name'], - [['somePerson', 'displayname'], 'update display name'], - [['somePerson', 'proxy'], 'update proxy'], - [['somePerson', 'propic'], 'update propic'], - [['somePerson'], 'member info'], - ])('%s returns correct values', async(args, expectedResult) => { - // Arrange - const authorId = '1'; - const authorFull = 'somePerson#0001'; - // Act - const result = await memberHelper.parseMemberCommand(authorId, authorFull, args); - // - expect(result).toEqual(expectedResult); - }); - - afterEach(() => { - // restore the spy created with spyOn - jest.restoreAllMocks(); - }); -}) - From d33c3213f3462930e74e99374e3360f6e1ff5f0f Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 21:20:56 -0500 Subject: [PATCH 39/59] removed confusing brackets from enum docs --- src/enums.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums.js b/src/enums.js index a9f0549..fa86092 100644 --- a/src/enums.js +++ b/src/enums.js @@ -35,7 +35,7 @@ helperEnums.help = { NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", - PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like .\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", + PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." } From 873959a5f49be263e6c8eb0fa104fb8499231cf7 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Wed, 18 Feb 2026 21:24:41 -0500 Subject: [PATCH 40/59] finally mocking correctly --- tests/helpers/webhookHelper.test.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/helpers/webhookHelper.test.js b/tests/helpers/webhookHelper.test.js index 2b5a2be..7c645a4 100644 --- a/tests/helpers/webhookHelper.test.js +++ b/tests/helpers/webhookHelper.test.js @@ -1,7 +1,6 @@ jest.mock('../../src/helpers/messageHelper.js') const {messageHelper} = require("../../src/helpers/messageHelper.js"); -const {Message, Webhook, Channel, Client} = require("@fluxerjs/core"); jest.mock('../../src/helpers/messageHelper.js', () => { return {messageHelper: { @@ -10,14 +9,12 @@ jest.mock('../../src/helpers/messageHelper.js', () => { }} }) -// jest.mock("@fluxerjs/core"); - const {webhookHelper} = require("../../src/helpers/webhookHelper.js"); const {enums} = require("../../src/enums"); describe('webhookHelper', () => { - const client = new Client(); + const client = {}; beforeEach(() => { jest.resetModules(); From 21efbccfd79199eda281cd27df0ec54526c4a08d Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 00:20:46 -0500 Subject: [PATCH 41/59] adding more cases to messageHelper tests --- src/helpers/messageHelper.js | 3 +-- tests/helpers/messageHelper.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index ac07535..dd301fc 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -57,8 +57,7 @@ msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ const splitProxy = member.proxy.split("text"); if(content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) { proxyMessage.member = member; - if (attachmentUrl) proxyMessage.hasAttachment = true; - + proxyMessage.hasAttachment = !!attachmentUrl; let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); escapedPrefix = new RegExp("^" + escapedPrefix); diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index aff235d..dff3d6c 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -68,13 +68,13 @@ describe('messageHelper', () => { test.each([ ['1', 'hello', null, {}], - ['1', '--hello', null, {member: membersFor1[0], message: 'hello'}], + ['1', '--hello', null, {member: membersFor1[0], message: 'hello', hasAttachment: false}], ['1', 'hello', attachmentUrl, {}], ['1', '--hello', attachmentUrl, {member: membersFor1[0], message: 'hello', hasAttachment: true}], ['1', '--', attachmentUrl, {member: membersFor1[0], message: '', hasAttachment: true}], - ['1', '?hello}', null, {member: membersFor1[3], message: 'hello'}], - ['1', '{hello}', null, {member: membersFor1[4], message: 'hello'}], - ['1', '⭐hello', null, {member: membersFor1[5], message: 'hello'}], + ['1', '?hello}', null, {member: membersFor1[3], message: 'hello', hasAttachment: false}], + ['1', '{hello}', null, {member: membersFor1[4], message: 'hello', hasAttachment: false}], + ['1', '⭐hello', null, {member: membersFor1[5], message: 'hello', hasAttachment: false}], ['2', 'hello', null, undefined], ['2', '--hello', null, undefined], ['2', 'hello', attachmentUrl, undefined], From a7cd4e96f069f0d4d3e52a6769fae600f73d9fd7 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 00:20:53 -0500 Subject: [PATCH 42/59] updating enums --- src/enums.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums.js b/src/enums.js index fa86092..7a58d17 100644 --- a/src/enums.js +++ b/src/enums.js @@ -36,7 +36,7 @@ helperEnums.help = { DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?)." + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?)." } helperEnums.misc = { From f9199f847701ccf32b39d8e8bad51c9be3289671 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 00:48:57 -0500 Subject: [PATCH 43/59] removed error response when proxy is sent without content --- src/helpers/messageHelper.js | 1 - tests/helpers/messageHelper.test.js | 8 -------- 2 files changed, 9 deletions(-) diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index dd301fc..b80676d 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -63,7 +63,6 @@ msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ escapedPrefix = new RegExp("^" + escapedPrefix); escapedSuffix = new RegExp(escapedSuffix + "$") proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, ""); - if (proxyMessage.message.length === 0 && !attachmentUrl) throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); } } }) diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index dff3d6c..9088c46 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -90,14 +90,6 @@ describe('messageHelper', () => { expect(res).toEqual(expected); }) }); - - test('expect error to be thrown when no message is present', () => { - // Act - return messageHelper.parseProxyTags('1', '', null).catch((res) => { - // Assert - expect(res).toEqual(new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY)); - }) - }) }) describe('returnBufferFromText', () => { From 9dab429d0dbba86793740de5bd5e8e72e422be93 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 00:51:33 -0500 Subject: [PATCH 44/59] , updated tests for webhookHelper and removed error response when proxy is sent without content --- src/helpers/webhookHelper.js | 30 +++--- tests/helpers/webhookHelper.test.js | 141 +++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 16 deletions(-) diff --git a/src/helpers/webhookHelper.js b/src/helpers/webhookHelper.js index 9aea380..23d9419 100644 --- a/src/helpers/webhookHelper.js +++ b/src/helpers/webhookHelper.js @@ -8,6 +8,7 @@ const name = 'PluralFlux Proxy Webhook'; /** * Replaces a proxied message with a webhook using the member information. + * @async * @param {Client} client - The fluxer.js client. * @param {Message} message - The full message object. * @throws {Error} When the proxy message is not in a server. @@ -16,7 +17,7 @@ wh.sendMessageAsMember = async function(client, message) { const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e}); // If the message doesn't match a proxy, just return. - if (!proxyMatch || !proxyMatch.member) { + if (!proxyMatch || !proxyMatch.member || (proxyMatch.message.length === 0 && !proxyMatch.hasAttachment) ) { return; } // If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs) @@ -31,6 +32,7 @@ wh.sendMessageAsMember = async function(client, message) { /** * Replaces a proxied message with a webhook using the member information. + * @async * @param {Client} client - The fluxer.js client. * @param {Message} message - The message to be deleted. * @param {string} text - The text to send via the webhook. @@ -38,26 +40,30 @@ wh.sendMessageAsMember = async function(client, message) { * @throws {Error} When there's no message to send. */ wh.replaceMessage = async function(client, message, text, member) { + // attachment logic is not relevant yet, text length will always be over 0 right now if (text.length > 0 || message.attachments.size > 0) { const channel = client.channels.get(message.channelId); const webhook = await wh.getOrCreateWebhook(client, channel).catch((e) =>{throw e}); const username = member.displayname ?? member.name; - await webhook.send({content: text, username: username, avatar_url: member.propic}).catch(async(e) => { - const returnedBuffer = messageHelper.returnBufferFromText(text); - await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.pdf', data: returnedBuffer.file }] - }) - console.error(e); - }); + if (text.length > 0) { + await webhook.send({content: text, username: username, avatar_url: member.propic}).catch(async(e) => { + const returnedBuffer = messageHelper.returnBufferFromText(text); + await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.txt', data: returnedBuffer.file }] + }) + console.error(e); + }); + } + if (message.attachments.size > 0) { + // Not implemented yet + } + await message.delete(); } - else { - throw new Error(enums.err.NO_MESSAGE_SENT_WITH_PROXY); - } } /** * Gets or creates a webhook. - * + * @async * @param {Client} client - The fluxer.js client. * @param {Channel} channel - The channel the message was sent in. * @returns {Webhook} A webhook object. @@ -75,7 +81,7 @@ wh.getOrCreateWebhook = async function(client, channel) { /** * Gets an existing webhook. - * + * @async * @param {Client} client - The fluxer.js client. * @param {Channel} channel - The channel the message was sent in. * @returns {Webhook} A webhook object. diff --git a/tests/helpers/webhookHelper.test.js b/tests/helpers/webhookHelper.test.js index 7c645a4..23a3aee 100644 --- a/tests/helpers/webhookHelper.test.js +++ b/tests/helpers/webhookHelper.test.js @@ -5,7 +5,8 @@ const {messageHelper} = require("../../src/helpers/messageHelper.js"); jest.mock('../../src/helpers/messageHelper.js', () => { return {messageHelper: { parseProxyTags: jest.fn(), - returnBuffer: jest.fn() + returnBuffer: jest.fn(), + returnBufferFromText: jest.fn(), }} }) @@ -14,16 +15,18 @@ const {enums} = require("../../src/enums"); describe('webhookHelper', () => { - const client = {}; - beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); }) describe(`sendMessageAsMember`, () => { + const client = {}; const content = "hi" - const attachments = new Map(); + const attachments = { + size: 0, + first: () => {} + } const message = { client, content: content, @@ -64,6 +67,7 @@ describe('webhookHelper', () => { expect(res).toBeUndefined(); expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1); expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null); + expect(webhookHelper.replaceMessage).not.toHaveBeenCalled(); }) }) @@ -94,6 +98,7 @@ describe('webhookHelper', () => { return webhookHelper.sendMessageAsMember(client, message).catch((res) => { // Assert expect(res).toEqual(new Error(enums.err.NOT_IN_SERVER)); + expect(webhookHelper.replaceMessage).not.toHaveBeenCalled(); }) }) @@ -112,11 +117,139 @@ describe('webhookHelper', () => { }) }) + test('if message matches member proxy and sent in a guild channel and no attachment, calls replace message', async() => { + // Arrange + message.guildId = '123'; + proxyMessage.hasAttachment = false; + messageHelper.parseProxyTags.mockResolvedValue(proxyMessage); + jest.spyOn(webhookHelper, 'replaceMessage').mockResolvedValue(); + // Act + return webhookHelper.sendMessageAsMember(client, message).then((res) => { + // Assert + expect(message.reply).not.toHaveBeenCalled(); + expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1); + expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member); + }) + }) + test('if replace message throws error, throw same error', async() => { + // Arrange + message.guildId = '123'; + messageHelper.parseProxyTags.mockResolvedValue(proxyMessage); + jest.spyOn(webhookHelper, 'replaceMessage').mockImplementation(() => {throw new Error("error")}); + // Act + return webhookHelper.sendMessageAsMember(client, message).catch((res) => { + // Assert + expect(message.reply).not.toHaveBeenCalled(); + expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1); + expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member); + expect(res).toEqual(new Error('error')); + }) + }) }) describe(`replaceMessage`, () => { + const channelId = '123'; + const authorId = '456'; + const guildId = '789'; + const text = "hello"; + const client = { + channels: { + get: jest.fn().mockReturnValue(channelId) + } + } + const member = {proxy: "--text", name: 'somePerson', displayname: "Some Person", propic: 'oya.png'}; + const attachments= { + size: 1, + first: () => {return channelId;} + }; + const message = { + client, + channelId: channelId, + content: text, + attachments: attachments, + author: { + id: authorId + }, + guild: { + guildId: guildId + }, + reply: jest.fn(), + delete: jest.fn() + } + const webhook = { + send: async() => jest.fn().mockResolvedValue() + } + + test('does not call anything if text is 0 or message has no attachments', async() => { + // Arrange + const emptyText = '' + const noAttachments = { + size: 0, + first: () => {} + } + message.attachments = noAttachments; + jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook); + // Act + return webhookHelper.replaceMessage(client, message, emptyText, member).then(() => { + expect(webhookHelper.getOrCreateWebhook).not.toHaveBeenCalled(); + expect(message.delete).not.toHaveBeenCalled(); + }) + }) + + test('calls getOrCreateWebhook and message.delete with correct arguments if text >= 0', async() => { + // Arrange + message.attachments = { + size: 0, + first: () => { + } + }; + jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook); + // Act + return webhookHelper.replaceMessage(client, message, text, member).then((res) => { + // Assert + expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1); + expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId); + expect(message.delete).toHaveBeenCalledTimes(1); + expect(message.delete).toHaveBeenCalledWith(); + }) + }) + + test('calls getOrCreateWebhook and message.delete with correct arguments if attachments exist', async() => { + // Arrange + const emptyText = '' + jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook); + // Act + return webhookHelper.replaceMessage(client, message, emptyText, member).then((res) => { + // Assert + expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1); + expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId); + expect(message.delete).toHaveBeenCalledTimes(1); + expect(message.delete).toHaveBeenCalledWith(); + }) + }) + + test('calls returnBufferFromText and console error if webhook.send returns error', async() => { + // Arrange + const file = Buffer.from(text, 'utf-8'); + const returnedBuffer = {text: text, file: file}; + const expected2ndSend = {content: returnedBuffer.text, username: member.displayname, avatar_url: member.propic, files: [{name: 'text.txt', data: returnedBuffer.file}]}; + jest.mock('console', () => ({error: jest.fn()})); + jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook); + webhook.send = jest.fn().mockImplementationOnce(async() => {throw new Error('error')}); + messageHelper.returnBufferFromText = jest.fn().mockResolvedValue(returnedBuffer); + // Act + return webhookHelper.replaceMessage(client, message, text, member).catch((res) => { + // Assert + expect(messageHelper.returnBufferFromText).toHaveBeenCalledTimes(1); + expect(messageHelper.returnBufferFromText).toHaveBeenCalledWith(text); + expect(webhook.send).toHaveBeenCalledTimes(2); + expect(webhook.send).toHaveBeenNthCalledWith(2, expected2ndSend); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(new Error('error')); + }) + }) }) describe(`getOrCreateWebhook`, () => { From 6eb9fef37680bd40febc6c23486d3bea3fbc081c Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 00:52:06 -0500 Subject: [PATCH 45/59] added debounce to count guilds properly --- src/bot.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/bot.js b/src/bot.js index 157f95d..decaea8 100644 --- a/src/bot.js +++ b/src/bot.js @@ -26,7 +26,7 @@ client.on(Events.MessageCreate, async (message) => { // If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return. if (!content.startsWith(messageHelper.prefix)) { - await webhookHelper.sendMessageAsMember(client, message).catch((e) => { + await webhookHelper.sendMessageAsMember(client, message, content).catch((e) => { throw e }); return; @@ -53,9 +53,28 @@ client.on(Events.MessageCreate, async (message) => { client.on(Events.Ready, () => { console.log(`Logged in as ${client.user?.username}`); - console.log(`Serving ${client.guilds.size} guild(s)`); }); +let guildCount = 0; +client.on(Events.GuildCreate, () => { + guildCount++; + callback(); +}); + +function printGuilds() { + console.log(`Serving ${client.guilds.size} guild(s)`); +} + +const callback = Debounce(printGuilds, 2000); + +function Debounce(func, delay) { + let timeout = null; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), delay); + }; +} + try { await client.login(token); // await db.check_connection(); From 7aeae1837f3ec06d84d7a3f6f36ee914c01976ef Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 00:59:57 -0500 Subject: [PATCH 46/59] added todo note --- tests/helpers/webhookHelper.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers/webhookHelper.test.js b/tests/helpers/webhookHelper.test.js index 23a3aee..560e176 100644 --- a/tests/helpers/webhookHelper.test.js +++ b/tests/helpers/webhookHelper.test.js @@ -216,6 +216,7 @@ describe('webhookHelper', () => { }) }) + // TODO: flaky for some reason test('calls getOrCreateWebhook and message.delete with correct arguments if attachments exist', async() => { // Arrange const emptyText = '' From 2e0a8adec50595b422dd93519a30d43cd2351adb Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 01:31:38 -0500 Subject: [PATCH 47/59] added tests for updateDisplayName --- src/helpers/memberHelper.js | 6 +-- tests/helpers/memberHelper.test.js | 65 ++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 721ece0..df8eafd 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -142,7 +142,7 @@ mh.updateName = async function (authorId, args) { * @throws {RangeError} When the display name is too long or doesn't exist. */ mh.updateDisplayName = async function (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { + if (args[2] && args[2] === "--help") { return enums.help.DISPLAY_NAME; } @@ -155,11 +155,11 @@ mh.updateDisplayName = async function (authorId, args) { if (member && member.displayname) { return `Display name for ${memberName} is: \"${member.displayname}\".`; } else if (member) { - throw new EmptyResultError(`Display name ${enums.err.NO_VALUE}`); + throw new Error(`Display name ${enums.err.NO_VALUE}`); } }); } else if (displayName.length > 32) { - throw new RangeError(enums.err.NO_VALUE); + throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); } else if (trimmedName === '') { throw new RangeError(`Display name ${enums.err.NO_VALUE}`); diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 3b6ff43..13dec44 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -238,15 +238,16 @@ describe('MemberHelper', () => { test('sends help message when --help parameter passed in', async () => { // Arrange const args = ['somePerson', 'displayname', '--help']; - + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); // Act return memberHelper.updateDisplayName(authorId, args).then((result) => { // Assert expect(result).toEqual(enums.help.DISPLAY_NAME); + expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); }) }) - test('Sends string of current displayname when no displayname passed in', async () => { + test('Sends string of current displayname when it exists and no displayname passed in', async () => { // Arrange const args = ['somePerson', 'displayname']; const displayname = "Some Person"; @@ -254,11 +255,69 @@ describe('MemberHelper', () => { displayname: displayname, } jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); - + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); // Act return memberHelper.updateDisplayName(authorId, args).then((result) => { // Assert expect(result).toEqual(`Display name for ${args[0]} is: "${member.displayname}".`); + expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); + }) + }) + + test('Sends error when no displayname passed in', async () => { + // Arrange + const args = ['somePerson', 'displayname']; + const member = {} + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); + // Act + return memberHelper.updateDisplayName(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new Error(`Display name ${enums.err.NO_VALUE}`)); + expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); + }) + }) + + test('Sends error when display name is too long', async () => { + // Arrange + const displayname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const args = ['somePerson', 'displayname', displayname]; + const member = {}; + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); + // Act + return memberHelper.updateDisplayName(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new RangeError(enums.err.DISPLAY_NAME_TOO_LONG)); + expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); + }) + }) + + test('Sends error when display name is blank', async () => { + // Arrange + const displayname = " "; + const args = ['somePerson', 'displayname', displayname]; + const member = {}; + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); + // Act + return memberHelper.updateDisplayName(authorId, args).catch((result) => { + // Assert + expect(result).toEqual(new Error(`Display name ${enums.err.NO_VALUE}`)); + expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); + }) + }) + + test('call updateMemberField with correct arguments when displayname passed in correctly', async() => { + // Arrange + const args = ['somePerson', 'displayname', "Some Person"]; + const member = {}; + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(member); + // Act + return memberHelper.updateDisplayName(authorId, args).then((result) => { + // Assert + expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, args); + expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1); }) }) }) From 7a3b8c199408c274e5d6273cb1c36166541a3065 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 01:45:43 -0500 Subject: [PATCH 48/59] edited help message trigger for updatePropic --- src/helpers/memberHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index df8eafd..a8f589c 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -206,7 +206,7 @@ mh.updateProxy = async function (authorId, args) { * @throws {Error} When loading the profile picture from a URL doesn't work. */ mh.updatePropic = async function (authorId, args, attachmentUrl, attachmentExpiry = null) { - if (args[1] && args[1] === "--help") { + if (args[2] && args[2] === "--help") { return enums.help.PROPIC; } let img; From 849acf72754ffb8574038472db12f064ca94bba1 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 18:54:35 -0500 Subject: [PATCH 49/59] update message helper test to include space case --- tests/helpers/messageHelper.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index 9088c46..b0e9488 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -45,9 +45,10 @@ describe('messageHelper', () => { const membersFor1 = [ {name: "somePerson", proxy: "--text"}, {name: "someSecondPerson", proxy: undefined}, -, {name: "someOtherPerson", proxy: "?text}"}, + {name: "someOtherPerson", proxy: "?text}"}, {name: "someLastPerson", proxy: "{text}"}, {name: "someEmojiPerson", proxy: "⭐text"}, + {name: "someSpacePerson", proxy: "! text"}, ] const membersFor2 = [] @@ -72,9 +73,10 @@ describe('messageHelper', () => { ['1', 'hello', attachmentUrl, {}], ['1', '--hello', attachmentUrl, {member: membersFor1[0], message: 'hello', hasAttachment: true}], ['1', '--', attachmentUrl, {member: membersFor1[0], message: '', hasAttachment: true}], - ['1', '?hello}', null, {member: membersFor1[3], message: 'hello', hasAttachment: false}], - ['1', '{hello}', null, {member: membersFor1[4], message: 'hello', hasAttachment: false}], - ['1', '⭐hello', null, {member: membersFor1[5], message: 'hello', hasAttachment: false}], + ['1', '?hello}', null, {member: membersFor1[2], message: 'hello', hasAttachment: false}], + ['1', '{hello}', null, {member: membersFor1[3], message: 'hello', hasAttachment: false}], + ['1', '⭐hello', null, {member: membersFor1[4], message: 'hello', hasAttachment: false}], + ['1', '! hello', null, {member: membersFor1[5], message: 'hello', hasAttachment: false}], ['2', 'hello', null, undefined], ['2', '--hello', null, undefined], ['2', 'hello', attachmentUrl, undefined], From 1e2724bbfb79b27c77493aa783da44ded5c1cceb Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 19:13:54 -0500 Subject: [PATCH 50/59] update bot to suppress errors from API --- src/bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index decaea8..b07058d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -47,7 +47,7 @@ client.on(Events.MessageCreate, async (message) => { } catch(error) { console.error(error); - return await message.reply(error.message); + // return await message.reply(error.message); } }); From c4c6ad0fddc4c25d35787f2e88dc980455e95137 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 19:31:18 -0500 Subject: [PATCH 51/59] fixed bug for import not sending help text, added help text if you type a unrecognized command --- src/bot.js | 3 +++ src/commands.js | 5 +++-- src/enums.js | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bot.js b/src/bot.js index b07058d..7a81c6c 100644 --- a/src/bot.js +++ b/src/bot.js @@ -44,6 +44,9 @@ client.on(Events.MessageCreate, async (message) => { throw e }); } + else { + await message.reply("Command not recognized. Try typing `pf;help` for command list."); + } } catch(error) { console.error(error); diff --git a/src/commands.js b/src/commands.js index 469314c..bdfd9ce 100644 --- a/src/commands.js +++ b/src/commands.js @@ -45,8 +45,9 @@ cmds.set('help', { cmds.set('import', { description: enums.help.SHORT_DESC_IMPORT, - async execute(message) { - if (message.content.includes('--help')) { + async execute(message, client, args) { + console.log(args); + if (message.content.includes('--help') || (args[0] === '' && args.length === 1)) { return await message.reply(enums.help.IMPORT); } const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; diff --git a/src/enums.js b/src/enums.js index 7a58d17..0cc2888 100644 --- a/src/enums.js +++ b/src/enums.js @@ -34,9 +34,9 @@ helperEnums.help = { LIST: "Lists members in the system. Currently only lists the first 25.", NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", - PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently.", + PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**", PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?)." + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**" } helperEnums.misc = { From fad6d42ee23dddd01cee3e3e3a91195fb65089dd Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 19:32:37 -0500 Subject: [PATCH 52/59] updated to be enum --- src/bot.js | 2 +- src/enums.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index 7a81c6c..d1e276d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -45,7 +45,7 @@ client.on(Events.MessageCreate, async (message) => { }); } else { - await message.reply("Command not recognized. Try typing `pf;help` for command list."); + await message.reply(enums.err.COMMAND_NOT_RECOGNIZED); } } catch(error) { diff --git a/src/enums.js b/src/enums.js index 0cc2888..0a72cb0 100644 --- a/src/enums.js +++ b/src/enums.js @@ -20,6 +20,7 @@ helperEnums.err = { NOT_JSON_FILE: "Please attach a valid JSON file.", NO_MEMBERS_IMPORTED: 'No members were imported.', IMPORT_ERROR: "Please see attached file for logs on the member import process.", + COMMAND_NOT_RECOGNIZED: "Command not recognized. Try typing `pf;help` for command list." } helperEnums.help = { From e7198230a7fe5268379eda9d28998b2c9ad55bdb Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 20:08:26 -0500 Subject: [PATCH 53/59] updated member helper and tests --- src/helpers/memberHelper.js | 168 ++++++++++++++++++---- tests/helpers/memberHelper.test.js | 214 ++++++++++++++++++++++++++--- 2 files changed, 336 insertions(+), 46 deletions(-) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index a8f589c..51c2f60 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -30,7 +30,7 @@ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUr case '--help': return enums.help.MEMBER; case 'new': - return await mh.addNewMember(authorId, args).catch((e) => { + return await mh.addNewMember(authorId, args, attachmentUrl).catch((e) => { throw e }); case 'remove': @@ -86,20 +86,21 @@ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUr * @async * @param {string} authorId - The author of the message * @param {string[]} args - The message arguments + * @param {string | null} attachmentURL - The attachment URL, if any exists * @returns {Promise} A successful addition. * @throws {Error} When the member exists, or creating a member doesn't work. */ -mh.addNewMember = async function (authorId, args) { +mh.addNewMember = async function (authorId, args, attachmentURL = null) { if (args[1] && args[1] === "--help" || !args[1]) { return enums.help.NEW; } const memberName = args[1]; const displayName = args[2]; + const proxy = args[3]; + const propic = args[4] ?? attachmentURL; - return await mh.addFullMember(authorId, memberName, displayName).then((member) => { - let success = `Member was successfully added.\nName: ${member.name}` - success += displayName ? `\nDisplay name: ${member.displayname}` : ""; - return success; + return await mh.addFullMember(authorId, memberName, displayName, proxy, propic).then(() => { + return mh.getMemberInfo(authorId, memberName).catch((e) => {throw e}) }).catch(e => { throw e; }) @@ -287,48 +288,157 @@ mh.removeMember = async function (authorId, args) { * @param {string | null} displayName - The display name of the member. * @param {string | null} proxy - The proxy tag of the member. * @param {string | null} propic - The profile picture URL of the member. - * @param {boolean} isImport - Whether calling from the import function or not. - * @returns {Promise} A successful addition. - * @throws {Error | RangeError} When the member already exists, there are validation errors, or adding a member doesn't work. + * @returns {Promise<{model, []}>} A successful addition object, including errors if there are any. + * @throws {Error} When the member already exists, there are validation errors, or adding a member doesn't work. */ -mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, isImport = false) { +mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null) { await mh.getMemberByName(authorId, memberName).then((member) => { if (member) { throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); } }); - if (displayName) { + const errors = []; + + let isValidDisplayName; + if (displayName && displayName.length > 0) { const trimmedName = displayName ? displayName.trim() : null; if (trimmedName && trimmedName.length > 32) { - throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); + errors.push(`Tried to set displayname to \"${displayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`); + isValidDisplayName = false; + } + else { + isValidDisplayName = true; } } - if (proxy) { - await mh.checkIfProxyExists(authorId, proxy).catch((e) => { - throw e + + let isValidProxy; + if (proxy && proxy.length > 0) { + await mh.checkIfProxyExists(authorId, proxy).then(() => { + isValidProxy = true; + }).catch((e) => { + errors.push(`Tried to set proxy to \"${proxy}\". ${e.message}. ${enums.err.SET_TO_NULL}`); + isValidProxy = false; }); } - let validPropic; - if (propic) { - validPropic = await mh.checkImageFormatValidity(propic).then((valid) => { - return valid; + + let isValidPropic; + if (propic && propic.length > 0) { + await mh.checkImageFormatValidity(propic).then(() => { + isValidPropic = true; }).catch((e) => { - if (!isImport) { - throw (e); - } - return false; + errors.push(`Tried to set profile picture to \"${propic}\". ${e.message}. ${enums.err.SET_TO_NULL}`); + isValidPropic = false; }); } - const member = await database.members.create({ - name: memberName, userid: authorId, displayname: displayName, proxy: proxy, propic: validPropic ? propic : null, + name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null }); - if (!member) { - new Error(`${enums.err.ADD_ERROR}`); - } - return member; + + return {member: member, errors: errors}; } +// mh.mergeFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null) { +// await mh.getMemberByName(authorId, memberName).then((member) => { +// if (member) { +// throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); +// } +// }); +// +// let isValidDisplayName; +// if (displayName) { +// const trimmedName = displayName ? displayName.trim() : null; +// if (trimmedName && trimmedName.length > 32) { +// if (!isImport) { +// throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); +// } +// isValidDisplayName = false; +// } +// } +// +// let isValidProxy; +// if (proxy) { +// isValidProxy = await mh.checkIfProxyExists(authorId, proxy).then((res) => { +// return res; +// }).catch((e) => { +// if (!isImport) { +// throw e +// } +// return false; +// }); +// } +// +// let isValidPropic; +// if (propic) { +// isValidPropic = await mh.checkImageFormatValidity(propic).then((valid) => { +// return valid; +// }).catch((e) => { +// if (!isImport) { +// throw (e); +// } +// return false; +// }); +// } +// +// const member = await database.members.create({ +// name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName: null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null, +// }); +// if (!member) { +// new Error(`${enums.err.ADD_ERROR}`); +// } +// return member; +// } +// +// mh.overwriteFullMemberFromImport = async function (authorId, memberName, displayName = null, proxy = null, propic = null) { +// await mh.getMemberByName(authorId, memberName).then((member) => { +// if (member) { +// throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); +// } +// }); +// +// let isValidDisplayName; +// if (displayName) { +// const trimmedName = displayName ? displayName.trim() : null; +// if (trimmedName && trimmedName.length > 32) { +// if (!isImport) { +// throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); +// } +// isValidDisplayName = false; +// } +// } +// +// let isValidProxy; +// if (proxy) { +// isValidProxy = await mh.checkIfProxyExists(authorId, proxy).then((res) => { +// return res; +// }).catch((e) => { +// if (!isImport) { +// throw e +// } +// return false; +// }); +// } +// +// let isValidPropic; +// if (propic) { +// isValidPropic = await mh.checkImageFormatValidity(propic).then((valid) => { +// return valid; +// }).catch((e) => { +// if (!isImport) { +// throw (e); +// } +// return false; +// }); +// } +// +// const member = await database.members.create({ +// name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName: null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null, +// }); +// if (!member) { +// new Error(`${enums.err.ADD_ERROR}`); +// } +// return member; +// } + /** * Updates one fields for a member in the database. * diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 13dec44..4442750 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -1,13 +1,24 @@ -jest.mock('@fluxerjs/core', () => jest.fn()); -jest.mock('../../src/database.js', () => jest.fn()); -jest.mock('sequelize', () => jest.fn()); - const {EmbedBuilder} = require("@fluxerjs/core"); const {database} = require('../../src/database.js'); const {enums} = require('../../src/enums.js'); const {EmptyResultError, Op} = require('sequelize'); const {memberHelper} = require("../../src/helpers/memberHelper.js"); +jest.mock('@fluxerjs/core', () => jest.fn()); +jest.mock('../../src/database.js', () => { + return { + database: { + members: { + create: jest.fn().mockResolvedValue(), + update: jest.fn().mockResolvedValue(), + destroy: jest.fn().mockResolvedValue(), + } + } + } +}); + +jest.mock('sequelize', () => jest.fn()); + describe('MemberHelper', () => { const authorId = "0001"; const authorFull = "author#0001"; @@ -34,7 +45,6 @@ describe('MemberHelper', () => { }); test.each([ - [['new'], 'new member', 'addNewMember', ['new']], [['remove'], 'remove member', 'removeMember', ['remove']], [['list'], 'all member info', 'getAllMembersInfo', authorFull], [['somePerson', 'name'], 'update name', 'updateName', ['somePerson', 'name']], @@ -42,7 +52,7 @@ describe('MemberHelper', () => { [['somePerson', 'proxy'], 'get proxy', 'getProxyByMember', 'somePerson'], [['somePerson', 'proxy', 'test'], 'update proxy', 'updateProxy', ['somePerson', 'proxy', 'test']], [['somePerson'], 'member info', 'getMemberInfo', 'somePerson'], - ])('%s calls methods and returns correct values', async (args, expectedResult, method, passedIn) => { + ])('%s calls %s and returns correct values', async (args, expectedResult, method, passedIn) => { // Act return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { // Assert @@ -52,7 +62,21 @@ describe('MemberHelper', () => { }); }); - test('["somePerson", "propic"] returns correct values and calls methods', () => { + + test.each([ + [['new'], attachmentUrl], + [['new'], null,] + ])('%s returns correct values and calls addNewMember', (args, attachmentUrl) => { + // Act + return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl).then((result) => { + // Assert + expect(result).toEqual("new member"); + expect(memberHelper.addNewMember).toHaveBeenCalledTimes(1); + expect(memberHelper.addNewMember).toHaveBeenCalledWith(authorId, args, attachmentUrl); + }); + }) + + test('["somePerson", "propic"] returns correct values and updatePropic', () => { // Arrange const args = ['somePerson', 'propic']; // Act @@ -98,7 +122,6 @@ describe('MemberHelper', () => { jest.spyOn(memberHelper, 'getProxyByMember').mockImplementation(() => { throw new Error('get proxy error')}); }) test.each([ - [['new'], 'new member error', 'addNewMember', ['new']], [['remove'], 'remove member error', 'removeMember', ['remove']], [['list'], 'all member info error', 'getAllMembersInfo', authorFull], [['somePerson', 'name'], 'update name error', 'updateName', ['somePerson', 'name']], @@ -115,6 +138,31 @@ describe('MemberHelper', () => { expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) }); }); + + test.each([ + [['new'], attachmentUrl], + [['new'], null,] + ])('%s throws correct error when addNewMember returns error', (args, attachmentUrl) => { + // Act + return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl).catch((result) => { + // Assert + expect(result).toEqual(new Error("new member error")); + expect(memberHelper.addNewMember).toHaveBeenCalledTimes(1); + expect(memberHelper.addNewMember).toHaveBeenCalledWith(authorId, args, attachmentUrl); + }); + }) + + test('["somePerson", "propic"] throws correct error when updatePropic returns error', () => { + // Arrange + const args = ['somePerson', 'propic']; + // Act + return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration).catch((result) => { + // Assert + expect(result).toEqual(new Error("update propic error")); + expect(memberHelper['updatePropic']).toHaveBeenCalledTimes(1); + expect(memberHelper['updatePropic']).toHaveBeenCalledWith(authorId, args, attachmentUrl, attachmentExpiration) + }); + }) }) }) @@ -131,29 +179,31 @@ describe('MemberHelper', () => { }) }) - test('returns member without display name when name passed in', async () => { + test('calls getMemberInfo when successful and returns result', async () => { // Arrange const args = ['new', 'some person']; const memberObject = { name: args[1] } - const expected = "Member was successfully added.\nName: " + args[1]; jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); + jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue(memberObject); //Act return memberHelper.addNewMember(authorId, args).then((result) => { // Assert - expect(result).toEqual(expected); + expect(result).toEqual(memberObject); + expect(memberHelper.getMemberInfo).toHaveBeenCalledTimes(1); + expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(authorId, args[1]); }) }) - test('returns member with display name when name and display name passed in', async () => { + test('throws expected error when getMemberInfo throws error', async () => { // Arrange - const args = ['new', 'some person', 'Some person Full Name']; - const memberObject = { name: args[1], displayname: args[2] } - const expected = "Member was successfully added.\nName: " + args[1] + "\nDisplay name: " + args[2]; + const args = ['new', 'some person']; + const memberObject = { name: args[1] } jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); + jest.spyOn(memberHelper, 'getMemberInfo').mockImplementation(() => { throw new Error('getMemberInfo error') }); //Act - return memberHelper.addNewMember(authorId, args).then((result) => { + return memberHelper.addNewMember(authorId, args).catch((result) => { // Assert - expect(result).toEqual(expected); + expect(result).toEqual(new Error('getMemberInfo error')); }) }) @@ -322,6 +372,136 @@ describe('MemberHelper', () => { }) }) + describe('addFullMember', () => { + const memberName = "somePerson"; + const displayName = "Some Person"; + const proxy = "--text"; + const propic = "oya.png"; + beforeEach(() => { + database.members.create = jest.fn().mockResolvedValue(); + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(); + }) + + test('calls getMemberByName', async() => { + // Act + return await memberHelper.addFullMember(authorId, memberName).then(() => { + // Assert + expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, memberName); + expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); + }) + }) + + test('if getMemberByName returns member, throw error', async() => { + memberHelper.getMemberByName.mockResolvedValue({name: memberName}); + // Act + return await memberHelper.addFullMember(authorId, memberName).catch((e) => { + // Assert + expect(e).toEqual(new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`)) + expect(database.members.create).not.toHaveBeenCalled(); + }) + }) + + test('if displayname is over 32 characters, call database.member.create with null value', async() => { + // Arrange + const displayName = "Some person with a very very very long name that can't be processed"; + const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: null} + database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + const expectedReturn = {member: expectedMemberArgs, errors: [`Tried to set displayname to \"${displayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`]} + + // Act + return await memberHelper.addFullMember(authorId, memberName, displayName, null, null).then((res) => { + // Assert + expect(res).toEqual(expectedReturn); + expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); + expect(database.members.create).toHaveBeenCalledTimes(1); + }) + }) + + test('if proxy, call checkIfProxyExists', async() => { + // Arrange + jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(); + const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: proxy, propic: null} + database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + const expectedReturn = {member: expectedMemberArgs, errors: []} + + // Act + return await memberHelper.addFullMember(authorId, memberName, null, proxy).then((res) => { + // Assert + expect(res).toEqual(expectedReturn); + expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, proxy); + expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1); + expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); + expect(database.members.create).toHaveBeenCalledTimes(1); + }) + }) + + test('if checkProxyExists throws error, call database.member.create with null value', async() => { + // Arrange + jest.spyOn(memberHelper, 'checkIfProxyExists').mockImplementation(() => {throw new Error('error')}); + const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: null} + database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + const expectedReturn = {member: expectedMemberArgs, errors: [`Tried to set proxy to \"${proxy}\". error. ${enums.err.SET_TO_NULL}`]} + + // Act + return await memberHelper.addFullMember(authorId, memberName, null, proxy, null).then((res) => { + // Assert + expect(res).toEqual(expectedReturn); + expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); + expect(database.members.create).toHaveBeenCalledTimes(1); + }) + }) + + test('if propic, call checkImageFormatValidity', async() => { + // Arrange + jest.spyOn(memberHelper, 'checkImageFormatValidity').mockResolvedValue(); + const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: propic} + database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + const expectedReturn = {member: expectedMemberArgs, errors: []} + // Act + return await memberHelper.addFullMember(authorId, memberName, null, null, propic).then((res) => { + // Assert + expect(res).toEqual(expectedReturn); + expect(memberHelper.checkImageFormatValidity).toHaveBeenCalledWith(propic); + expect(memberHelper.checkImageFormatValidity).toHaveBeenCalledTimes(1); + expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); + expect(database.members.create).toHaveBeenCalledTimes(1); + }) + }) + + test('if checkImageFormatValidity throws error, call database.member.create with null value', async() => { + // Arrange + jest.spyOn(memberHelper, 'checkImageFormatValidity').mockImplementation(() => {throw new Error('error')}); + const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: null} + database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + const expectedReturn = {member: expectedMemberArgs, errors: [`Tried to set profile picture to \"${propic}\". error. ${enums.err.SET_TO_NULL}`]} + // Act + return await memberHelper.addFullMember(authorId, memberName, null, null, propic).then((res) => { + // Assert + expect(res).toEqual(expectedReturn); + expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); + expect(database.members.create).toHaveBeenCalledTimes(1); + }) + }) + + test('if all values are valid, call database.members.create', async() => { + // Arrange + jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(); + jest.spyOn(memberHelper, 'checkImageFormatValidity').mockResolvedValue(); + const expectedMemberArgs = {name: memberName, userid: authorId, displayname: displayName, proxy: proxy, propic: propic} + database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + const expectedReturn = {member: expectedMemberArgs, errors: []} + // Act + // Act + return await memberHelper.addFullMember(authorId, memberName, displayName, proxy, propic).then((res) => { + // Assert + expect(res).toEqual(expectedReturn); + expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); + expect(database.members.create).toHaveBeenCalledTimes(1); + }) + }) + + }) + afterEach(() => { // restore the spy created with spyOn jest.restoreAllMocks(); From b1a29fd3ff56f80ac79227cea227958329243005 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 20:30:55 -0500 Subject: [PATCH 54/59] edit enums, tweak import content command --- src/commands.js | 8 ++++---- src/enums.js | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/commands.js b/src/commands.js index bdfd9ce..4e2673e 100644 --- a/src/commands.js +++ b/src/commands.js @@ -12,7 +12,7 @@ cmds.set('member', { const authorFull = `${message.author.username}#${message.author.discriminator}` const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; const attachmentExpires = message.attachments.size > 0 ? message.attachments.first().expires_at : null; - const reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires).catch(e =>{throw e}); + const reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires).catch(async (e) =>{await message.reply(e.message);}); if (typeof reply === 'string') { return await message.reply(reply); } @@ -47,11 +47,11 @@ cmds.set('import', { description: enums.help.SHORT_DESC_IMPORT, async execute(message, client, args) { console.log(args); - if (message.content.includes('--help') || (args[0] === '' && args.length === 1)) { + + const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; + if ((message.content.includes('--help') || (args[0] === '' && args.length === 1)) && !attachmentUrl ) { return await message.reply(enums.help.IMPORT); } - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; - return await importHelper.pluralKitImport(message.author.id, attachmentUrl).then(async (successfullyAdded) => { await message.reply(successfullyAdded); }).catch(async (error) => { diff --git a/src/enums.js b/src/enums.js index 0a72cb0..a0bc118 100644 --- a/src/enums.js +++ b/src/enums.js @@ -7,7 +7,7 @@ helperEnums.err = { ADD_ERROR: "Error adding member.", MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", USER_NO_MEMBERS: "You have no members created.", - DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.", + DISPLAY_NAME_TOO_LONG: "The maximum length of a display name is 32 characters.", PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", NO_SUCH_COMMAND: "No such command exists.", PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", @@ -37,7 +37,8 @@ helperEnums.help = { DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**", PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**" + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**", + SET_TO_NULL: "It has been set to null instead." } helperEnums.misc = { From d6403223927aa77f22aa8d6e0f0621c7b12c2f9c Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 20:32:35 -0500 Subject: [PATCH 55/59] removed unnecessary await and console.log --- src/commands.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commands.js b/src/commands.js index 4e2673e..bec40bc 100644 --- a/src/commands.js +++ b/src/commands.js @@ -46,8 +46,6 @@ cmds.set('help', { cmds.set('import', { description: enums.help.SHORT_DESC_IMPORT, async execute(message, client, args) { - console.log(args); - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; if ((message.content.includes('--help') || (args[0] === '' && args.length === 1)) && !attachmentUrl ) { return await message.reply(enums.help.IMPORT); @@ -60,7 +58,7 @@ cmds.set('import', { let errorsText = `${error.message}.\nThese errors occurred:\n${error.errors.join('\n')}`; await message.reply(errorsText).catch(async () => { - const returnedBuffer = await messageHelper.returnBufferFromText(errorsText); + const returnedBuffer = messageHelper.returnBufferFromText(errorsText); await message.reply({content: returnedBuffer.text, files: [{ name: 'text.pdf', data: returnedBuffer.file }] }) }); From b64ba6e787e6cdf551283536a51376b670a382bc Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 21:36:19 -0500 Subject: [PATCH 56/59] made it easier to make a member --- src/commands.js | 4 ++++ src/helpers/memberHelper.js | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/commands.js b/src/commands.js index bec40bc..5a7edc1 100644 --- a/src/commands.js +++ b/src/commands.js @@ -19,6 +19,10 @@ cmds.set('member', { else if (reply instanceof EmbedBuilder) { await message.reply({embeds: [reply.toJSON()]}) } + else if (typeof reply === 'object') { + const errorsText = reply.errors.length > 0 ? reply.errors.join('\n- ') : null; + return await message.reply({content: `${reply.success} ${errorsText ? "\nThese errors occurred:\n" + errorsText : ""}`, embeds: [reply.embed.toJSON()]}) + } } }) diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index 51c2f60..72c3442 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -17,7 +17,9 @@ const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', ' * @param {string[]} args - The message arguments * @param {string | null} attachmentUrl - The message attachment url. * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) - * @returns {Promise | Promise } A message, or an informational embed. + * @returns {Promise} A success message. + * @returns {Promise } A list of 25 members as an embed. + * @returns {Promise<{EmbedBuilder, [], string}>} A member info embed + info/errors. * @throws {Error} */ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) { @@ -99,8 +101,9 @@ mh.addNewMember = async function (authorId, args, attachmentURL = null) { const proxy = args[3]; const propic = args[4] ?? attachmentURL; - return await mh.addFullMember(authorId, memberName, displayName, proxy, propic).then(() => { - return mh.getMemberInfo(authorId, memberName).catch((e) => {throw e}) + return await mh.addFullMember(authorId, memberName, displayName, proxy, propic).then(async(response) => { + const memberInfoEmbed = await mh.getMemberInfo(authorId, memberName).catch((e) => {throw e}) + return {embed: memberInfoEmbed, errors: response.errors, success: `${memberName} has been added successfully.`}; }).catch(e => { throw e; }) From eb6c60618b418f171bcb8aaff72e70bce372414a Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 21:38:03 -0500 Subject: [PATCH 57/59] added nicer error listing to importHelper --- src/helpers/importHelper.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/helpers/importHelper.js b/src/helpers/importHelper.js index cb520ce..6cb2b15 100644 --- a/src/helpers/importHelper.js +++ b/src/helpers/importHelper.js @@ -18,17 +18,19 @@ ih.pluralKitImport = async function (authorId, attachmentUrl) { } return fetch(attachmentUrl).then((res) => res.json()).then(async(pkData) => { const pkMembers = pkData.members; - const errors = []; + let errors = []; const addedMembers = []; for (let pkMember of pkMembers) { const proxy = pkMember.proxy_tags[0] ? `${pkMember.proxy_tags[0].prefix ?? ''}text${pkMember.proxy_tags[0].suffix ?? ''}` : null; - await memberHelper.addFullMember(authorId, pkMember.name, pkMember.display_name, proxy, pkMember.avatar_url, true).then((member) => { - addedMembers.push(member.name); + await memberHelper.addFullMember(authorId, pkMember.name, pkMember.display_name, proxy, pkMember.avatar_url).then((memberObj) => { + addedMembers.push(memberObj.member.name); + if (memberObj.errors.length > 0) { + errors.push(`\n**${pkMember.name}:** `) + errors = errors.concat(memberObj.errors); + } }).catch(e => { - errors.push(`${pkMember.name}: ${e.message}`); + errors.push(e.message); }); - await memberHelper.checkImageFormatValidity(pkMember.avatar_url).catch(e => { - errors.push(`${pkMember.name}: ${e.message}`)}); } const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED; if (errors.length > 0) { From 4d3db8c0016b4f76e5b0734342939094844010a4 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 21:38:10 -0500 Subject: [PATCH 58/59] updated documentation --- README.md | 17 +++++++++++++---- src/enums.js | 8 ++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 804f01a..2b5a5d2 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,20 @@ All commands are prefixed by `pf;`. Currently only a few are implemented. - `pf;help` - Sends the current list of commands. -- `pf;import` - Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is *TBD*. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently (the stuff above), not anything else like birthdays or system handles (yet?). +- `pf;import` - Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is *TBD*. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**" - `pf;member` - Accesses the sub-commands related to editing proxy members. The available subcommands are: - - `new` - Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. -You can optionally add a display name after the member name, for example: `pf;member new jane "Jane Doe | ze/hir"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters. + - `new` - Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. The order of values is `pf;member new [name] [displayname] [proxy] [propic]`, _without brackets_. The name is **required**, but the rest are optional. + Usage notes: + - If anything has spaces, put it in quotes: `"Jane Doe"` + - If anything is unset, and you want to set something after it (for ex: you haven't set a display name, but you want to add a proxy), put the unset value in empty quotes in the same position: "" If you leave it out, the bot will set things wrong. + - The maximum length of a display name is 32 characters. + - You can't use the same proxy for two different members. + - You can also upload an image directly instead of using a url. + Examples: + - Full example: `pf;member new jane "Jane Doe" J:text https://cdn.pixabay.com/photo/2023/10/20/19/07/aster-8330078_1280.jpg` + - Example with gaps: `pf;member new bob "Bob he/him" "" https://cdn.pixabay.com/photo/2016/05/09/11/09/tennis-1381230_1280.jpg + - `remove` - Removes a member based on their name, for example: `pf;member remove jane`. - `name` - Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily. - `list` - Lists all members in the system. @@ -25,7 +34,7 @@ You can optionally add a display name after the member name, for example: `pf;me 1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like . 2. Upload an attachment directly. **NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended. - - `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. Only one proxy can be set per member currently. + - `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.** ## Notes - Attaching files to messages with the proxy does not work, due to either the limitations of Fluxer itself :( diff --git a/src/enums.js b/src/enums.js index a0bc118..ea11c6d 100644 --- a/src/enums.js +++ b/src/enums.js @@ -20,7 +20,8 @@ helperEnums.err = { NOT_JSON_FILE: "Please attach a valid JSON file.", NO_MEMBERS_IMPORTED: 'No members were imported.', IMPORT_ERROR: "Please see attached file for logs on the member import process.", - COMMAND_NOT_RECOGNIZED: "Command not recognized. Try typing `pf;help` for command list." + COMMAND_NOT_RECOGNIZED: "Command not recognized. Try typing `pf;help` for command list.", + SET_TO_NULL: "It has been set to null instead." } helperEnums.help = { @@ -30,15 +31,14 @@ helperEnums.help = { SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.", PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.", MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.", - NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in __double quotes__. The length limit is 32 characters.", + NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nThe order of values is `pf;member new [name] [displayname] [proxy] [propic]`, _without brackets_. The name is **required**, but the rest are optional.\nUsage notes:\n- If anything has spaces, put it in quotes.\n- If anything is unset and you want to set something after it (for ex: you haven't set a display name but you want to add a proxy), put the unset value in empty quotes in the same position: \"\" If you leave it out, the bot will set things wrong.\n- The maximum length of a display name is 32 characters.\n- You can't use the same proxy for two different members.\n- You can also upload an image directly instead of using a url.\nExamples:\n- Everything filled out: `pf;member new jane \"Jane Doe\" J:text https://cdn.pixabay.com/photo/2023/10/20/19/07/aster-8330078_1280.jpg`\n- Example with gaps: `pf;member new bob \"Bob he/him\" \"\" https://cdn.pixabay.com/photo/2016/05/09/11/09/tennis-1381230_1280.jpg`", REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.", LIST: "Lists members in the system. Currently only lists the first 25.", NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.", PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**", PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**", - SET_TO_NULL: "It has been set to null instead." + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**" } helperEnums.misc = { From a30d408c02032ad03b9205a04cffc24a757430f4 Mon Sep 17 00:00:00 2001 From: Aster Fialla Date: Thu, 19 Feb 2026 21:41:59 -0500 Subject: [PATCH 59/59] Merge branch 'main' of https://github.com/pieartsy/PluralFlux into add-tests --- src/bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index d1e276d..3a89b31 100644 --- a/src/bot.js +++ b/src/bot.js @@ -34,7 +34,7 @@ client.on(Events.MessageCreate, async (message) => { const commandName = content.slice(messageHelper.prefix.length).split(" ")[0]; // If there's no command name (ie just the prefix) - if (!commandName) await message.reply(enums.help.SHORT_DESC_PLURALFLUX); + if (!commandName) return await message.reply(enums.help.SHORT_DESC_PLURALFLUX); const args = messageHelper.parseCommandArgs(content, commandName);