forked from PluralFlux/PluralFlux
Compare commits
157 Commits
more-disco
...
add-attach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
899d04a125 | ||
| 8fc590c062 | |||
|
|
d24bcc8438 | ||
|
|
eb80fd2ec0 | ||
|
|
f65aeb0019 | ||
|
|
7a3b8c1994 | ||
|
|
2e0a8adec5 | ||
|
|
7aeae1837f | ||
|
|
6eb9fef376 | ||
|
|
9dab429d0d | ||
|
|
f9199f8477 | ||
|
|
a7cd4e96f0 | ||
|
|
21efbccfd7 | ||
|
|
873959a5f4 | ||
|
|
d33c3213f3 | ||
|
|
75c4c548d8 | ||
|
|
9d5493e8ab | ||
|
|
fc1c463696 | ||
|
|
1bba8099e9 | ||
|
|
acd9ce7c3e | ||
|
|
da9a3d2c8a | ||
|
|
274f1ead15 | ||
|
|
223292c2d3 | ||
|
|
400e40a405 | ||
|
|
152bc8873d | ||
|
|
e16694ac2d | ||
|
|
f0ac02e86d | ||
|
|
5c01f2e284 | ||
|
|
da5a250445 | ||
|
|
23a57b3e99 | ||
|
|
1bf6c8c1f2 | ||
|
|
fe00f66104 | ||
|
|
15703c24cd | ||
|
|
3dbbe7df50 | ||
|
|
31eb4262dd | ||
|
|
c645bb0aea | ||
|
|
0b7f549bdf | ||
|
|
bfc633a755 | ||
|
|
01e620a935 | ||
|
|
a4804c2ea7 | ||
|
|
164ff7d8b6 | ||
|
|
5e3b3f33d3 | ||
|
|
4fcb53482c | ||
|
|
ba9552b4aa | ||
|
|
35b454bc80 | ||
|
|
321fe7f0a9 | ||
|
|
0a4bfa59ad | ||
|
|
79d98c3618 | ||
|
|
a44e2745c5 | ||
|
|
5ab0d62bdb | ||
|
|
4f2e893491 | ||
|
|
74cab4f91f | ||
|
|
1661dfd637 | ||
|
|
876f9486ad | ||
|
|
5e28cdfd01 | ||
|
|
5a39610547 | ||
|
|
a3caa2dc42 | ||
|
|
055ecdf20d | ||
|
|
51ada567ce | ||
|
|
be5505b03c | ||
|
|
07d4e735eb | ||
|
|
8b709b75ce | ||
|
|
6cd34e9c68 | ||
|
|
b2fc9f9111 | ||
|
|
b3813d771d | ||
|
|
f19a5ba58c | ||
|
|
5a9a2977c6 | ||
|
|
b15262e1c5 | ||
|
|
4ea0a777af | ||
|
|
a3c3eb1545 | ||
|
|
559da55176 | ||
|
|
f169f4d755 | ||
|
|
32adf7b6ef | ||
|
|
1db4ab37dc | ||
|
|
fa0de17724 | ||
|
|
99bfdf685b | ||
|
|
0a7057ef63 | ||
|
|
58912f382c | ||
|
|
375601cca7 | ||
|
|
e67a10fa79 | ||
|
|
b83325785f | ||
|
|
49cab523f0 | ||
|
|
e1dbba9043 | ||
|
|
fa86606fb4 | ||
|
|
cf37508ee4 | ||
|
|
38105d910e | ||
|
|
fe05b93ac7 | ||
|
|
964b5ec32a | ||
|
|
cebf14a2f5 | ||
|
|
dafbafeec6 | ||
|
|
8f70960079 | ||
|
|
75dba413b8 | ||
|
|
f81da5ac27 | ||
|
|
be83e8d629 | ||
|
|
9bff32456c | ||
|
|
ba623b0b78 | ||
|
|
85d876f660 | ||
|
|
db5a7398ce | ||
|
|
ff84a637b2 | ||
|
|
8762e41a0a | ||
|
|
3512f07def | ||
|
|
e08ae7b8ae | ||
|
|
63594c9819 | ||
|
|
fc7309630f | ||
|
|
5d80895918 | ||
|
|
2e63532dc7 | ||
|
|
5c1d974246 | ||
|
|
dedd50adfc | ||
|
|
d512e11682 | ||
|
|
be864a4d1b | ||
|
|
7264fe0b1c | ||
|
|
33ddc58c41 | ||
|
|
e729cc770b | ||
|
|
5c6b76b59b | ||
|
|
f0a36f430c | ||
|
|
625be55328 | ||
|
|
163bad02df | ||
|
|
9919e8c4b1 | ||
|
|
70ee8180ab | ||
|
|
55e34b1178 | ||
|
|
e88f66b2dc | ||
|
|
647474e2c2 | ||
|
|
7ef2adc689 | ||
|
|
83a2497ff4 | ||
|
|
89fe2c70b2 | ||
|
|
7c7b1f0202 | ||
|
|
3d36e6c9fc | ||
|
|
623d10a17e | ||
|
|
a2c0530118 | ||
|
|
68629fd75d | ||
|
|
6339da1592 | ||
|
|
d2007f5274 | ||
|
|
c894002018 | ||
|
|
3e0ba190c1 | ||
|
|
12e93ce69d | ||
|
|
415bf44f57 | ||
|
|
e602eefc5d | ||
|
|
650f39266b | ||
|
|
bbc566b8b9 | ||
|
|
62ef2b87cf | ||
|
|
bf155d28b4 | ||
|
|
d473a5119a | ||
|
|
e634085627 | ||
|
|
3bec40e235 | ||
|
|
ba91ecd097 | ||
|
|
23d7abef5d | ||
|
|
0708622045 | ||
|
|
0d129f23e7 | ||
|
|
851bae4ff8 | ||
|
|
a92f00e545 | ||
|
|
8b3be16cea | ||
|
|
1d0c8ef4e7 | ||
|
|
f7a7906532 | ||
|
|
b3f565bd83 | ||
|
|
9b73997c05 | ||
|
|
1b81e4007b | ||
|
|
64535b6a63 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: pieartsy
|
||||
39
.github/workflows/node.js.yml
vendored
Normal file
39
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [25.3.0]
|
||||
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
working-directory: tests
|
||||
|
||||
- name: Tests failed
|
||||
if: failure()
|
||||
run: exit 1
|
||||
|
||||
- name: Tests passed
|
||||
run: npm run build --if-present
|
||||
working-directory: src
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
node_modules
|
||||
.idea
|
||||
secrets/
|
||||
package-lock.json
|
||||
config.json
|
||||
config.json
|
||||
coverage
|
||||
log.txt
|
||||
.env
|
||||
oya.png
|
||||
25
Dockerfile
25
Dockerfile
@@ -1,21 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
|
||||
USER $APP_UID
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["PluralFlux.csproj", "./"]
|
||||
RUN dotnet restore "PluralFlux.csproj"
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
WORKDIR "/src/"
|
||||
RUN dotnet build "./PluralFlux.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./PluralFlux.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "PluralFlux.dll"]
|
||||
CMD ["node", "src/bot.js"]
|
||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# PluralFlux
|
||||
PluralFlux is a proxybot akin to PluralKit and Tupperbox, but for [Fluxer](https://fluxer.app/). It is written with the [Fluxer.js](https://fluxerjs.blstmo.com/) library.
|
||||
|
||||
[Invite it to your server](https://web.fluxer.app/oauth2/authorize?client_id=1471588659706540815&scope=bot&permissions=4503600164498496).
|
||||
|
||||
[Join the support server](https://fluxer.gg/WaO6qGdU)
|
||||
|
||||
[Sponsor the project](https://github.com/sponsors/pieartsy)
|
||||
|
||||
If it's not running at the moment, it's because my computer crashed or something. I'm looking to move running it to a somewhat more permanent solution.
|
||||
|
||||
## Commands
|
||||
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, 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. 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.
|
||||
- `displayname` - 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__.
|
||||
- `propic` - Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:
|
||||
1. 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/>.
|
||||
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.**
|
||||
|
||||
## Notes
|
||||
- Only one proxy tag can be set per member currently.
|
||||
|
||||
## Upcoming
|
||||
- [ ] React with x to delete message
|
||||
- [ ] System tag at the end of messages
|
||||
- [ ] Optionally keep proxy tag in message
|
||||
- [ ] Autoproxy front
|
||||
8
babel.config.js
Normal file
8
babel.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// babel.config.js
|
||||
module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
plugins: ["@babel/plugin-transform-modules-commonjs"]
|
||||
}
|
||||
}
|
||||
};
|
||||
78
bot.js
78
bot.js
@@ -1,78 +0,0 @@
|
||||
import { Client, GatewayDispatchEvents } from "@discordjs/core";
|
||||
import { REST } from "@discordjs/rest";
|
||||
import { WebSocketManager } from "@discordjs/ws";
|
||||
import { db } from './sequelize.js';
|
||||
import { webhookHelper } from "./helpers/webhookHelper.js";
|
||||
import { messageHelper } from "./helpers/messageHelper.js";
|
||||
import { memberHelper } from "./helpers/memberHelper.js";
|
||||
import {enums} from "./enums.js";
|
||||
|
||||
const token = process.env.FLUXER_BOT_TOKEN;
|
||||
|
||||
if (!token) {
|
||||
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rest = new REST({
|
||||
api: "https://api.fluxer.app",
|
||||
version: "1",
|
||||
}).setToken(token);
|
||||
|
||||
const gateway = new WebSocketManager({
|
||||
token,
|
||||
intents: 0, // Fluxer has no intents yet
|
||||
rest,
|
||||
version: "1",
|
||||
});
|
||||
|
||||
export const client = new Client({ rest, gateway });
|
||||
|
||||
let pluralFluxName = "PluralFlux";
|
||||
let pluralFluxDiscriminator = "8677";
|
||||
|
||||
client.on(GatewayDispatchEvents.MessageCreate, async ({ api, data }) => {
|
||||
try {
|
||||
if (data.webhook_id) {
|
||||
return;
|
||||
} else if (data.author.username === pluralFluxName && data.author.discriminator === pluralFluxDiscriminator) {
|
||||
return;
|
||||
} else if (data.content.startsWith(messageHelper.prefix)) {
|
||||
|
||||
const commandName = data.content.slice(messageHelper.prefix.length).split(" ")[0];
|
||||
const args = messageHelper.parseCommandArgs(data.content, commandName);
|
||||
if (!commandName) {
|
||||
return await api.channels.createMessage(data.channel_id, {content: enums.help.PLURALFLUX});
|
||||
}
|
||||
switch (commandName) {
|
||||
case 'm':
|
||||
case 'member':
|
||||
const attachment = data.attachments[0] ?? null;
|
||||
const reply = await memberHelper.parseMemberCommand(data.author.id, args, attachment);
|
||||
return await api.channels.createMessage(data.channel_id, {content: reply});
|
||||
case 'help':
|
||||
return await api.channels.createMessage(data.channel_id, {content: enums.help.PLURALFLUX});
|
||||
default:
|
||||
return await api.channels.createMessage(data.channel_id, {content: enums.err.NO_SUCH_COMMAND});
|
||||
}
|
||||
const proxyMatch = await messageHelper.parseProxyTags(data.author.id, data.content);
|
||||
if (!proxyMatch.proxy) {
|
||||
return;
|
||||
}
|
||||
const member = await memberHelper.getMemberByProxy(data.author.id, proxyMatch.proxy);
|
||||
await webhookHelper.replaceMessage(api, data, proxyMatch.message, member);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
return await api.channels.createMessage(data.channel_id, {content: error});
|
||||
}
|
||||
});
|
||||
|
||||
client.on(GatewayDispatchEvents.Ready, async ({data}) => {
|
||||
console.log(`Logged in as ${data.user.username}#${data.user.discriminator}`);
|
||||
pluralFluxName = data.user.username;
|
||||
pluralFluxDiscriminator = data.user.discriminator;
|
||||
await db.check_connection();
|
||||
});
|
||||
|
||||
await gateway.connect();
|
||||
41
compose.yaml
41
compose.yaml
@@ -1,4 +1,10 @@
|
||||
services:
|
||||
main:
|
||||
build: .
|
||||
container_name: pluralflux
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- pluralflux-net
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
container_name: pluralflux-postgres
|
||||
@@ -10,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:
|
||||
|
||||
27
enums.js
27
enums.js
@@ -1,27 +0,0 @@
|
||||
const helperEnums = {};
|
||||
|
||||
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 or PNG format.",
|
||||
PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL."
|
||||
}
|
||||
|
||||
helperEnums.help = {
|
||||
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.\n\nThe current commands are: `pf;member` and `pf;help`.",
|
||||
MEMBER: "You can shorten this command to `pf;m`. The available subcommands for `pf;member` are `add`, `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.",
|
||||
ADD: "Creates a new member to proxy with, for example: `pf;member jane`. The member name should ideally be short so you can write other commands with it. \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`.",
|
||||
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 quotes.",
|
||||
PROXY: "Updates the proxy tag for a specific member based on their name, for example: `pf;member jane proxy Jane:` or `pf;member amal proxy A=`. This is put at *the start* of a message to allow it to be proxied. Proxies that wrap around text or go at the end are *not* currently supported.",
|
||||
PROPIC: "Updates the profile picture for the member. Must be in JPG or PNG format. 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.",
|
||||
}
|
||||
|
||||
export const enums = helperEnums;
|
||||
@@ -1,301 +0,0 @@
|
||||
import { db } from '../sequelize.js';
|
||||
import {enums} from "../enums.js";
|
||||
import { loadImage } from "canvas";
|
||||
|
||||
const mh = {};
|
||||
|
||||
// Has an empty "command" to parse the help message properly
|
||||
const commandList = ['--help', 'add', 'remove', 'displayName', 'proxy', 'propic', ''];
|
||||
|
||||
/**
|
||||
* Parses through the subcommands that come after "pf;member" and calls functions accordingly.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @param {string} attachment - The message attachments
|
||||
* @returns {Promise<string>} A message.
|
||||
*/
|
||||
mh.parseMemberCommand = async function(authorId, args, attachment){
|
||||
console.log(authorId, args);
|
||||
let member;
|
||||
// checks whether command is in list, otherwise assumes it's a name
|
||||
if(!commandList.includes(args[0])) {
|
||||
member = await getMemberInfo(authorId, args[0]);
|
||||
if (member === enums.err.NO_MEMBER) {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
switch(args[0]) {
|
||||
case '--help':
|
||||
return enums.help.MEMBER;
|
||||
case 'add':
|
||||
return await addNewMember(authorId, args);
|
||||
case 'remove':
|
||||
return await removeMember(authorId, args);
|
||||
case 'displayname':
|
||||
return enums.help.DISPLAY_NAME;
|
||||
case 'proxy':
|
||||
return enums.help.PROXY;
|
||||
case 'propic':
|
||||
return enums.help.PROPIC;
|
||||
case '':
|
||||
return enums.help.MEMBER;
|
||||
}
|
||||
switch(args[1]) {
|
||||
case '--help':
|
||||
return enums.help.MEMBER;
|
||||
case 'displayname':
|
||||
return await updateDisplayName(authorId, args);
|
||||
case 'proxy':
|
||||
return await updateProxy(authorId, args);
|
||||
case 'propic':
|
||||
return await updatePropic(authorId, args, attachment)
|
||||
default:
|
||||
return member;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a member, first checking that there is no member of that name associated with the author.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} A successful addition, or an error message.
|
||||
*/
|
||||
async function addNewMember(authorId, args) {
|
||||
if (args[1] && args[1] === "--help" || !args[1]) {
|
||||
return enums.help.ADD;
|
||||
}
|
||||
const memberName = args[1];
|
||||
const displayName = args[2];
|
||||
|
||||
const member = await getMemberInfo(authorId, memberName);
|
||||
if (member && member !== enums.err.NO_MEMBER) {
|
||||
return enums.err.MEMBER_EXISTS;
|
||||
}
|
||||
const trimmedName = displayName ? displayName.replaceAll(' ', '') : null;
|
||||
return await db.members.create({
|
||||
name: memberName,
|
||||
userid: authorId,
|
||||
displayname: trimmedName !== null ? displayName : null,
|
||||
}).then((m) => {
|
||||
let success = `Member was successfully added.\nName: ${m.dataValues.name}`
|
||||
success += displayName ? `\nDisplay name: ${m.dataValues.displayname}` : "";
|
||||
return success;
|
||||
}).catch(e => {
|
||||
return `${enums.err.ADD_ERROR}: ${e.message}`;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display name for a member.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} A successful update, or an error message.
|
||||
*/
|
||||
async function updateDisplayName(authorId, args) {
|
||||
if (args[1] && args[1] === "--help" || !args[1]) {
|
||||
return enums.help.DISPLAY_NAME;
|
||||
}
|
||||
|
||||
const memberName = args[0];
|
||||
const displayName = args[2];
|
||||
const trimmed_name = displayName ? displayName.replaceAll(' ', '') : null;
|
||||
|
||||
if (!displayName || trimmed_name === null ) {
|
||||
let member = await mh.getMemberByName(authorId, memberName);
|
||||
if (member.displayname) {
|
||||
return `Display name for ${memberName} is: \"${member.displayname}\".`;
|
||||
}
|
||||
return `Display name ${enums.err.NO_VALUE}`
|
||||
}
|
||||
else if (displayName.length > 32) {
|
||||
return enums.err.DISPLAY_NAME_TOO_LONG;
|
||||
}
|
||||
console.log(displayName);
|
||||
return updateMember(authorId, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the proxy for a member, first checking that no other members attached to the author have the tag.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string> } A successful update, or an error message.
|
||||
*/
|
||||
async function updateProxy(authorId, args) {
|
||||
if (args[1] && args[1] === "--help" || !args[1]) {
|
||||
return enums.help.PROXY;
|
||||
}
|
||||
const proxy = args[2];
|
||||
const trimmedProxy = proxy ? proxy.replaceAll(' ', '') : null;
|
||||
|
||||
if (trimmedProxy == null) {
|
||||
return `Proxy ${enums.err.NO_VALUE}`;
|
||||
}
|
||||
|
||||
const members = await mh.getMembersByAuthor(authorId);
|
||||
const proxyExists = members.some(member => member.proxy === proxy);
|
||||
if (proxyExists) {
|
||||
return enums.err.PROXY_EXISTS;
|
||||
}
|
||||
return updateMember(authorId, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the profile pic for a member, based on either the attachment or the args provided.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @param {string} attachment - The url of the first attachment in the message
|
||||
* @returns {Promise<string> } A successful update, or an error message.
|
||||
*/
|
||||
async function updatePropic(authorId, args, attachment) {
|
||||
if (args[1] && args[1] === "--help") {
|
||||
return enums.help.PROPIC;
|
||||
}
|
||||
let img;
|
||||
const updatedArgs = args;
|
||||
if (!updatedArgs[1] && !attachment) {
|
||||
return enums.help.PROPIC;
|
||||
} else if (attachment) {
|
||||
updatedArgs[2] = attachment.url;
|
||||
updatedArgs[3] = attachment.expires_at;
|
||||
}
|
||||
if (updatedArgs[2]) {
|
||||
img = updatedArgs[2];
|
||||
}
|
||||
|
||||
return await loadImage(img).then(() => {
|
||||
return updateMember(authorId, updatedArgs);
|
||||
}).catch((err) => {
|
||||
return `${enums.err.PROPIC_CANNOT_LOAD}: ${err.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a member.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} A successful removal, or an error message.
|
||||
*/
|
||||
async function removeMember(authorId, args) {
|
||||
if (args[1] && args[1] === "--help") {
|
||||
return enums.help.REMOVE;
|
||||
}
|
||||
|
||||
const memberName = args[1];
|
||||
if (!memberName) {
|
||||
return `${enums.err.NO_NAME_PROVIDED} deletion.`;
|
||||
}
|
||||
return await db.members.destroy({ where: { name: memberName, userid: authorId } }).then(() => {
|
||||
return `Member "${memberName}" has been deleted.`;
|
||||
}).catch(e => {
|
||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
/*======Non-Subcommands======*/
|
||||
|
||||
/**
|
||||
* Updates a member's fields in the database.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} A successful update, or an error message.
|
||||
*/
|
||||
async function updateMember(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]) {
|
||||
console.log(args);
|
||||
fluxerPropicWarning = setExpirationWarning(args[3]);
|
||||
}
|
||||
return await db.members.update({[columnName]: value}, { where: { name: memberName, userid: authorId } }).then(() => {
|
||||
return `Updated ${columnName} for ${memberName} to "${value}"${fluxerPropicWarning ?? ''}.`;
|
||||
}).catch(e => {
|
||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the warning for an expiration date.
|
||||
*
|
||||
* @param {string} expirationString - An expiration date string.
|
||||
* @returns {string} A successful update, or an error message.
|
||||
*/
|
||||
function setExpirationWarning(expirationString) {
|
||||
let expirationDate = new Date(expirationString);
|
||||
console.log(expirationDate, expirationDate instanceof Date);
|
||||
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 <https://imgbb.com/> and link to it directly.`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the details for a member.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string} memberName - The message arguments
|
||||
* @returns {Promise<string>} The member's info, or an error message.
|
||||
*/
|
||||
async function getMemberInfo(authorId, memberName) {
|
||||
let member = await db.members.findOne({ where: { name: memberName, userid: authorId } });
|
||||
if (member) {
|
||||
let member_info = `Member name: ${member.name}`;
|
||||
member_info += member.displayname ? `\nDisplay name: ${member.displayname}` : '\nDisplay name: unset';
|
||||
member_info += member.proxy ? `\nProxy Tag: ${member.proxy}` : '\nProxy tag: unset';
|
||||
member_info += member.propic ? `\nProfile pic: ${member.propic}` : '\nProfile pic: unset';
|
||||
return member_info;
|
||||
}
|
||||
return enums.err.NO_MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member based on the author and proxy tag.
|
||||
*
|
||||
* @param {string} authorId - The author of the message.
|
||||
* @param {string} name - The member's name.
|
||||
* @returns {Promise<model> | Promise<string>} The member object, or an error message.
|
||||
*/
|
||||
mh.getMemberByName = async function(authorId, name) {
|
||||
return await db.members.findOne({ where: { userid: authorId, name: name } }).catch(e => {
|
||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member based on the author and proxy tag.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string} proxy - The proxy tag
|
||||
* @returns {Promise<model> | Promise<string>} The member object, or an error message.
|
||||
*/
|
||||
mh.getMemberByProxy = async function(authorId, proxy) {
|
||||
return await db.members.findOne({ where: { userid: authorId, proxy: proxy } }).catch(e => {
|
||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all members belonging to the author.
|
||||
*
|
||||
* @param {string} authorId - The author of the message
|
||||
* @returns {Promise<model[]> | Promise<string>} The member object, or an error message.
|
||||
*/
|
||||
mh.getMembersByAuthor = async function(authorId) {
|
||||
return await db.members.findAll({ where: { userid: authorId } }).catch(e => {
|
||||
// I have no idea how this could possibly happen but better safe than sorry
|
||||
return `${enums.err.USER_NO_MEMBERS}: ${e.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const memberHelper = mh;
|
||||
@@ -1,51 +0,0 @@
|
||||
import {memberHelper} from "./memberHelper.js";
|
||||
|
||||
const msgh = {};
|
||||
|
||||
msgh.prefix = "pf;"
|
||||
|
||||
/**
|
||||
* Parses and slices up message arguments, retaining quoted strings.
|
||||
*
|
||||
* @param {string} text - The full message content.
|
||||
* @param {string} commandName - The command name.
|
||||
* @returns {string[]} An array of arguments.
|
||||
*/
|
||||
msgh.parseCommandArgs = function(text, commandName) {
|
||||
const message = text.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
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses proxy tags and sees if they match the tags of any member belonging to an author.
|
||||
*
|
||||
* @param {string} authorId - The author of the message.
|
||||
* @param {string} text - The full message content.
|
||||
* @returns {Object} The proxy message object.
|
||||
*/
|
||||
msgh.parseProxyTags = async function (authorId, text){
|
||||
const members = await memberHelper.getMembersByAuthor(authorId);
|
||||
const proxyMessage = {}
|
||||
members.forEach(member => {
|
||||
if (text.startsWith(member.proxy) && text.length > member.proxy.length) {
|
||||
proxyMessage.proxy = member.proxy;
|
||||
proxyMessage.message = text.slice(member.proxy.length).trim();
|
||||
}
|
||||
})
|
||||
return proxyMessage;
|
||||
}
|
||||
|
||||
export const messageHelper = msgh;
|
||||
@@ -1,60 +0,0 @@
|
||||
const wh = {};
|
||||
|
||||
/**
|
||||
* Gets or creates a webhook.
|
||||
*
|
||||
* @param api - The discord.js API.
|
||||
* @param {string} channelId - The channel the message was sent in.
|
||||
* @returns {Object} A webhook object.
|
||||
*/
|
||||
wh.getOrCreateWebhook = async function (api, channelId) {
|
||||
const name = 'PluralFlux Proxy Webhook';
|
||||
let webhook = await getWebhook(api, channelId, name);
|
||||
if (!webhook) {
|
||||
webhook = await api.channels.createWebhook(channelId, {name: name});
|
||||
}
|
||||
return webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an existing webhook.
|
||||
*
|
||||
* @param api - The discord.js API.
|
||||
* @param {string} channelId - The channel the message was sent in.
|
||||
* @param {string} name - The name of the webhook.
|
||||
* @returns {Object} A webhook object.
|
||||
*/
|
||||
async function getWebhook(api, channelId, name) {
|
||||
const allWebhooks = await api.channels.getWebhooks(channelId);
|
||||
if (allWebhooks.length === 0) {
|
||||
return;
|
||||
}
|
||||
let pf_webhook;
|
||||
allWebhooks.forEach((webhook) => {
|
||||
if (webhook.name === name) {
|
||||
pf_webhook = webhook;
|
||||
}
|
||||
})
|
||||
return pf_webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a proxied message with a webhook using the member information.
|
||||
*
|
||||
* @param api - The discord.js API.
|
||||
* @param data - The discord.js data.
|
||||
* @param {string} text - The text to send via the webhook.
|
||||
* @param {Object} member - A member object from the database.
|
||||
*/
|
||||
wh.replaceMessage = async function (api, data, text, member) {
|
||||
if (text.length > 0) {
|
||||
const webhook = await wh.getOrCreateWebhook(api, data.channel_id);
|
||||
await api.webhooks.execute(webhook.id, webhook.token, {content: text, username: member.displayname ?? member.name, avatar_url: member.propic});
|
||||
await api.channels.deleteMessage(data.channel_id, data.id);
|
||||
}
|
||||
else {
|
||||
await api.channels.createMessage(data.channel_id, {content: '(Please input a message!)'});
|
||||
}
|
||||
}
|
||||
|
||||
export const webhookHelper = wh;
|
||||
10
jest.config.js
Normal file
10
jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
collectCoverage: true,
|
||||
coverageDirectory: "coverage",
|
||||
verbose: true,
|
||||
transform: {
|
||||
"^.+\\.[t|j]sx?$": require.resolve('babel-jest')
|
||||
},
|
||||
};
|
||||
7758
package-lock.json
generated
Normal file
7758
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -2,24 +2,30 @@
|
||||
"name": "pluralflux",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "bot.js",
|
||||
"type": "module",
|
||||
"main": "src/bot.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pieartsy/PluralFlux.git"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.4.0",
|
||||
"@discordjs/rest": "^2.6.0",
|
||||
"@discordjs/ws": "^2.0.4",
|
||||
"canvas": "^3.2.1",
|
||||
"@fluxerjs/core": "^1.1.5",
|
||||
"dotenv": "^17.3.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pg": "^8.18.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.7"
|
||||
"pm2": "^6.0.14",
|
||||
"sequelize": "^6.37.7",
|
||||
"tmp": "^0.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.0.0"
|
||||
"@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": "jest"
|
||||
}
|
||||
}
|
||||
|
||||
87
src/bot.js
Normal file
87
src/bot.js
Normal file
@@ -0,0 +1,87 @@
|
||||
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();
|
||||
|
||||
const token = process.env.FLUXER_BOT_TOKEN;
|
||||
|
||||
if (!token) {
|
||||
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new Client({ intents: 0 });
|
||||
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
try {
|
||||
// Ignore bots and messages without content
|
||||
if (message.author.bot || !message.content) return;
|
||||
|
||||
// Parse command and arguments
|
||||
const content = message.content.trim();
|
||||
|
||||
// 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) => {
|
||||
throw e
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const commandName = content.slice(messageHelper.prefix.length).split(" ")[0];
|
||||
// If there's no command name (ie just the prefix)
|
||||
if (!commandName) return await message.reply(enums.help.SHORT_DESC_PLURALFLUX);
|
||||
|
||||
const args = messageHelper.parseCommandArgs(content, commandName);
|
||||
|
||||
const command = commands.get(commandName);
|
||||
if (command) {
|
||||
await command.execute(message, client, args).catch(e => {
|
||||
throw e
|
||||
});
|
||||
}
|
||||
else {
|
||||
await message.reply(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
// return await message.reply(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
client.on(Events.Ready, () => {
|
||||
console.log(`Logged in as ${client.user?.username}`);
|
||||
});
|
||||
|
||||
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();
|
||||
} catch (err) {
|
||||
console.error('Login failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
78
src/commands.js
Normal file
78
src/commands.js
Normal file
@@ -0,0 +1,78 @@
|
||||
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 "./helpers/importHelper.js";
|
||||
|
||||
const cmds = new Map();
|
||||
|
||||
cmds.set('member', {
|
||||
description: enums.help.SHORT_DESC_MEMBER,
|
||||
async execute(message, client, args) {
|
||||
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(async (e) =>{await message.reply(e.message);});
|
||||
if (typeof reply === 'string') {
|
||||
return await message.reply(reply);
|
||||
}
|
||||
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()]})
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
cmds.set('help', {
|
||||
description: enums.help.SHORT_DESC_HELP,
|
||||
async execute(message) {
|
||||
const fields = [...cmds.entries()].map(([name, cmd]) => ({
|
||||
name: `${messageHelper.prefix}${name}`,
|
||||
value: cmd.description,
|
||||
inline: true,
|
||||
}));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Commands')
|
||||
.setDescription(enums.help.PLURALFLUX)
|
||||
.addFields(...fields)
|
||||
.setFooter({ text: `Prefix: ${messageHelper.prefix}` })
|
||||
.setTimestamp();
|
||||
|
||||
await message.reply({ embeds: [embed.toJSON()] });
|
||||
},
|
||||
})
|
||||
|
||||
cmds.set('import', {
|
||||
description: enums.help.SHORT_DESC_IMPORT,
|
||||
async execute(message, client, 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);
|
||||
}
|
||||
return await importHelper.pluralKitImport(message.author.id, attachmentUrl).then(async (successfullyAdded) => {
|
||||
await message.reply(successfullyAdded);
|
||||
}).catch(async (error) => {
|
||||
if (error instanceof AggregateError) {
|
||||
// errors.message can be a list of successfully added members, or say that none were successful.
|
||||
let errorsText = `${error.message}.\nThese errors occurred:\n${error.errors.join('\n')}`;
|
||||
|
||||
await message.reply(errorsText).catch(async () => {
|
||||
const returnedBuffer = messageHelper.returnBufferFromText(errorsText);
|
||||
await message.reply({content: returnedBuffer.text, files: [{ name: 'text.pdf', data: returnedBuffer.file }]
|
||||
})
|
||||
});
|
||||
}
|
||||
// If just one error was returned.
|
||||
else {
|
||||
return await message.reply(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const commands = cmds;
|
||||
@@ -1,4 +1,7 @@
|
||||
import {DataTypes, Sequelize} from 'sequelize';
|
||||
import * as env from 'dotenv';
|
||||
|
||||
env.config();
|
||||
|
||||
const password = process.env.POSTGRES_PASSWORD;
|
||||
|
||||
@@ -7,17 +10,18 @@ if (!password) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const database = {};
|
||||
const db = {};
|
||||
|
||||
const sequelize = new Sequelize('postgres', 'postgres', password, {
|
||||
host: 'localhost',
|
||||
logging: false,
|
||||
dialect: 'postgres'
|
||||
});
|
||||
|
||||
database.sequelize = sequelize;
|
||||
database.Sequelize = Sequelize;
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
database.members = sequelize.define('Member', {
|
||||
db.members = sequelize.define('Member', {
|
||||
userid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
@@ -37,8 +41,26 @@ database.members = sequelize.define('Member', {
|
||||
}
|
||||
});
|
||||
|
||||
database.check_connection = async function() {
|
||||
await sequelize.authenticate().then(async (result) => {
|
||||
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.
|
||||
*/
|
||||
db.check_connection = async function() {
|
||||
await sequelize.authenticate().then(async () => {
|
||||
console.log('Connection has been established successfully.');
|
||||
await syncModels();
|
||||
}).catch(err => {
|
||||
@@ -47,8 +69,11 @@ database.check_connection = async function() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs Sequelize models.
|
||||
*/
|
||||
async function syncModels() {
|
||||
await sequelize.sync().then((result) => {
|
||||
await sequelize.sync().then(() => {
|
||||
console.log('Models synced successfully.');
|
||||
}).catch((err) => {
|
||||
console.error('Syncing models did not work', err);
|
||||
@@ -56,4 +81,4 @@ async function syncModels() {
|
||||
});
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
export const database = db;
|
||||
48
src/enums.js
Normal file
48
src/enums.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const helperEnums = {};
|
||||
|
||||
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 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.",
|
||||
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.",
|
||||
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 = {
|
||||
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\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.**"
|
||||
}
|
||||
|
||||
helperEnums.misc = {
|
||||
ATTACHMENT_SENT_BY: "Attachment sent by:"
|
||||
}
|
||||
|
||||
export const enums = helperEnums;
|
||||
43
src/helpers/importHelper.js
Normal file
43
src/helpers/importHelper.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import {enums} from "../enums.js";
|
||||
import {memberHelper} from "./memberHelper.js";
|
||||
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
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).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(e.message);
|
||||
});
|
||||
}
|
||||
const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED;
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors, aggregatedText);
|
||||
}
|
||||
return aggregatedText;
|
||||
});
|
||||
}
|
||||
|
||||
export const importHelper = ih;
|
||||
618
src/helpers/memberHelper.js
Normal file
618
src/helpers/memberHelper.js
Normal file
@@ -0,0 +1,618 @@
|
||||
import {database} from '../database.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<string>} A success message.
|
||||
* @returns {Promise <EmbedBuilder>} 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) {
|
||||
let member;
|
||||
// checks whether command is in list, otherwise assumes it's a name
|
||||
if (!commandList.includes(args[0]) && !args[1]) {
|
||||
member = await mh.getMemberInfo(authorId, args[0]);
|
||||
}
|
||||
switch (args[0]) {
|
||||
case '--help':
|
||||
return enums.help.MEMBER;
|
||||
case 'new':
|
||||
return await mh.addNewMember(authorId, args, attachmentUrl).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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a member.
|
||||
*
|
||||
* @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<string>} A successful addition.
|
||||
* @throws {Error} When the member exists, or creating a member doesn't work.
|
||||
*/
|
||||
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, 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;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the name for a member.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} A successful update.
|
||||
* @throws {RangeError} When the name doesn't exist.
|
||||
*/
|
||||
mh.updateName = async function (authorId, args) {
|
||||
if (args[2] && args[2] === "--help") {
|
||||
return enums.help.NAME;
|
||||
}
|
||||
|
||||
const name = args[2];
|
||||
if (!name) {
|
||||
return `The name for ${args[0]} is ${args[0]}, 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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display name for a member.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} A successful update.
|
||||
* @throws {RangeError} When the display name is too long or doesn't exist.
|
||||
*/
|
||||
mh.updateDisplayName = async function (authorId, args) {
|
||||
if (args[2] && args[2] === "--help") {
|
||||
return enums.help.DISPLAY_NAME;
|
||||
}
|
||||
|
||||
const memberName = args[0];
|
||||
const displayName = args[2];
|
||||
const trimmedName = displayName ? displayName.trim() : 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 Error(`Display name ${enums.err.NO_VALUE}`);
|
||||
}
|
||||
});
|
||||
} else if (displayName.length > 32) {
|
||||
throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG);
|
||||
}
|
||||
else if (trimmedName === '') {
|
||||
throw new RangeError(`Display name ${enums.err.NO_VALUE}`);
|
||||
}
|
||||
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<string> } 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<string>} 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[2] && args[2] === "--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<boolean>} - 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<string>} 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
|
||||
}
|
||||
}).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.
|
||||
* @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) {
|
||||
await mh.getMemberByName(authorId, memberName).then((member) => {
|
||||
if (member) {
|
||||
throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`);
|
||||
}
|
||||
});
|
||||
const errors = [];
|
||||
|
||||
let isValidDisplayName;
|
||||
if (displayName && displayName.length > 0) {
|
||||
const trimmedName = displayName ? displayName.trim() : null;
|
||||
if (trimmedName && trimmedName.length > 32) {
|
||||
errors.push(`Tried to set displayname to \"${displayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`);
|
||||
isValidDisplayName = false;
|
||||
}
|
||||
else {
|
||||
isValidDisplayName = true;
|
||||
}
|
||||
}
|
||||
|
||||
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 isValidPropic;
|
||||
if (propic && propic.length > 0) {
|
||||
await mh.checkImageFormatValidity(propic).then(() => {
|
||||
isValidPropic = true;
|
||||
}).catch((e) => {
|
||||
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: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
|
||||
});
|
||||
|
||||
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.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string[]} args - The message arguments
|
||||
* @returns {Promise<string>} 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
|
||||
}
|
||||
}).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 ?? ''}.`;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <https://imgbb.com/> 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<EmbedBuilder>} 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<EmbedBuilder>} 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<model>} 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<string>} 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.proxy;
|
||||
}
|
||||
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<model>} 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<model[] | null>} 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<boolean> } 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;
|
||||
139
src/helpers/messageHelper.js
Normal file
139
src/helpers/messageHelper.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import {memberHelper} from "./memberHelper.js";
|
||||
import tmp, {setGracefulCleanup} from "tmp";
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const msgh = {};
|
||||
|
||||
msgh.prefix = "pf;"
|
||||
|
||||
setGracefulCleanup();
|
||||
|
||||
/**
|
||||
* 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 === '\"' || 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 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 {{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){
|
||||
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;
|
||||
proxyMessage.hasAttachment = !!attachmentUrl;
|
||||
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, "");
|
||||
}
|
||||
}
|
||||
})
|
||||
return proxyMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a text message that's too long as its text plus a file with the remaining text.
|
||||
*
|
||||
* @param {string} text - The text of the message.
|
||||
* @returns {{text: string, file: Buffer<ArrayBuffer> | undefined}} The text and buffer object
|
||||
*
|
||||
*/
|
||||
msgh.returnBufferFromText = function (text) {
|
||||
if (text.length > 2000) {
|
||||
const truncated = text.substring(0, 2000);
|
||||
const restOfText = text.substring(2000);
|
||||
const file = Buffer.from(restOfText, 'utf-8');
|
||||
return {text: truncated, file: file}
|
||||
}
|
||||
return {text: text, file: undefined}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ArrayBuffer from an attachment URL.
|
||||
*
|
||||
* @param {string} attachmentUrl
|
||||
* @returns {ArrayBuffer} The buffer from the image.
|
||||
*
|
||||
*/
|
||||
msgh.returnBufferFromUrl = async function (attachmentUrl) {
|
||||
retryPromise(() => fetch(attachmentUrl),{
|
||||
retryIf: (response) => !response.ok,
|
||||
retries: 5
|
||||
}).then(async(res) => {
|
||||
return await res.arrayBuffer().catch((err) => {
|
||||
throw new Error(`Error loading attachment into buffer: ${err.message}`);
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Source - https://stackoverflow.com/a/70687149 - Arturo Hernandez
|
||||
function retryPromise(promise, options) {
|
||||
const { retryIf, retryCatchIf, retries } = { retryIf: () => false, retryCatchIf: () => true, retries: 5, ...options};
|
||||
let _promise = promise();
|
||||
|
||||
for (let i = 1; i < retries; i++)
|
||||
_promise = _promise.catch((value) => retryCatchIf(value) ? promise() : Promise.reject(value))
|
||||
.then((value) => retryIf(value) ? promise() : Promise.reject(value));
|
||||
|
||||
return _promise;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an ArrayBuffer from an attachment URL.
|
||||
*
|
||||
* @param {Collection<string, APIMessageAttachment>} attachments - A collection of attachments from the message object
|
||||
* @returns {[{string, ArrayBuffer}]} An array of file objects
|
||||
*
|
||||
*/
|
||||
msgh.createFileObjectFromAttachments = async function (attachments) {
|
||||
if (attachments.size === 0) {
|
||||
return [];
|
||||
}
|
||||
const attachmentsObj = [];
|
||||
attachments.forEach(async (attachment) => {
|
||||
await msgh.returnBufferFromUrl(attachment.url).then((res) => {
|
||||
attachmentsObj.push({name: attachment.filename, data: res});
|
||||
});
|
||||
});
|
||||
return attachmentsObj;
|
||||
}
|
||||
|
||||
export const messageHelper = msgh;
|
||||
109
src/helpers/webhookHelper.js
Normal file
109
src/helpers/webhookHelper.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import {messageHelper} from "./messageHelper.js";
|
||||
import {Webhook, Channel, Message, Client} from '@fluxerjs/core';
|
||||
import {enums} from "../enums.js";
|
||||
|
||||
const wh = {};
|
||||
|
||||
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.
|
||||
*/
|
||||
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 || (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)
|
||||
if (!message.guildId) {
|
||||
throw new Error(enums.err.NOT_IN_SERVER);
|
||||
}
|
||||
const attachments = messageHelper.createFileObjectFromAttachments(message.attachments);
|
||||
|
||||
await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member, attachments).catch(e =>{throw e});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {model} member - A member object from the database.
|
||||
* @param {[{string, ArrayBuffer}]} attachments - Attachments file objects, if any.
|
||||
* @throws {Error} When there's no message to send.
|
||||
*/
|
||||
wh.replaceMessage = async function (client, message, text, member, attachments) {
|
||||
if (text.length === 0 && attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
const channel = client.channels.get(message.channelId);
|
||||
const webhook = await wh.getOrCreateWebhook(client, channel).catch((e) => {
|
||||
throw e
|
||||
});
|
||||
const username = member.displayname ?? member.name;
|
||||
if (text.length > 0) {
|
||||
if (text.length > 2000) {
|
||||
const returnedBuffer = messageHelper.returnBufferFromText(text);
|
||||
await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.txt', data: returnedBuffer.file }]
|
||||
})
|
||||
attachments.push(returnedBuffer);
|
||||
}
|
||||
await webhook.send({content: text, username: username, avatar_url: member.propic, files: attachments}).catch(async (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
else {
|
||||
await webhook.send({username: username, avatar_url: member.propic, files: attachments}).catch(async (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
await message.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @throws {Error} When no webhooks are allowed in the 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 wh.getWebhook(client, channel).catch((e) =>{throw e});
|
||||
if (!webhook) {
|
||||
webhook = await channel.createWebhook({name: name});
|
||||
}
|
||||
return webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
wh.getWebhook = async function(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;
|
||||
510
tests/helpers/memberHelper.test.js
Normal file
510
tests/helpers/memberHelper.test.js
Normal file
@@ -0,0 +1,510 @@
|
||||
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";
|
||||
const attachmentUrl = "../oya.png";
|
||||
const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z')
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
})
|
||||
|
||||
describe('parseMemberCommand', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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("get proxy");
|
||||
});
|
||||
|
||||
test.each([
|
||||
[['remove'], 'remove member', 'removeMember', ['remove']],
|
||||
[['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', 'test'], 'update proxy', 'updateProxy', ['somePerson', 'proxy', 'test']],
|
||||
[['somePerson'], 'member info', 'getMemberInfo', 'somePerson'],
|
||||
])('%s calls %s 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)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
});
|
||||
})
|
||||
|
||||
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 enums', async (args, expectedResult) => {
|
||||
// Arrange
|
||||
const authorId = '1';
|
||||
const authorFull = 'somePerson#0001';
|
||||
// Act
|
||||
return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => {
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
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([
|
||||
[['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 calls methods and throws correct values', async (args, expectedError, method, passedIn) => {
|
||||
// Act
|
||||
return 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)
|
||||
});
|
||||
});
|
||||
|
||||
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)
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addNewMember', () => {
|
||||
|
||||
test('returns help if --help passed in', async() => {
|
||||
// Arrange
|
||||
const args = ['new', '--help'];
|
||||
const expected = enums.help.NEW;
|
||||
//Act
|
||||
return memberHelper.addNewMember(authorId, args).then((result) => {
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
})
|
||||
})
|
||||
|
||||
test('calls getMemberInfo when successful and returns result', async () => {
|
||||
// Arrange
|
||||
const args = ['new', 'some person'];
|
||||
const memberObject = { name: 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(memberObject);
|
||||
expect(memberHelper.getMemberInfo).toHaveBeenCalledTimes(1);
|
||||
expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(authorId, args[1]);
|
||||
})
|
||||
})
|
||||
|
||||
test('throws expected error when getMemberInfo throws error', async () => {
|
||||
// Arrange
|
||||
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).catch((result) => {
|
||||
// Assert
|
||||
expect(result).toEqual(new Error('getMemberInfo error'));
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
return 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
|
||||
return 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'];
|
||||
const expected = `The name for ${args[0]} is ${args[0]}, but you probably knew that!`;
|
||||
|
||||
// Act
|
||||
return memberHelper.updateName(authorId, args).then((result) => {
|
||||
expect(result).toEqual(expected);
|
||||
})
|
||||
})
|
||||
|
||||
test('throws error when name is empty', async () => {
|
||||
// Arrange
|
||||
const args = ['somePerson', 'name', " "];
|
||||
|
||||
// Act
|
||||
return 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
|
||||
return memberHelper.updateName(authorId, args).catch((result) => {
|
||||
// Assert
|
||||
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
|
||||
return memberHelper.updateName(authorId, args).then((result) => {
|
||||
// Assert
|
||||
expect(result).toEqual("Updated");
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateDisplayName', () => {
|
||||
|
||||
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 it exists and no displayname passed in', async () => {
|
||||
// Arrange
|
||||
const args = ['somePerson', 'displayname'];
|
||||
const displayname = "Some Person";
|
||||
const member = {
|
||||
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);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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();
|
||||
});
|
||||
})
|
||||
|
||||
128
tests/helpers/messageHelper.test.js
Normal file
128
tests/helpers/messageHelper.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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 fetch = require('node-fetch');
|
||||
|
||||
jest.mock('../../src/helpers/memberHelper.js', () => {
|
||||
return {memberHelper: {
|
||||
getMembersByAuthor: jest.fn()
|
||||
}}
|
||||
})
|
||||
|
||||
jest.mock('node-fetch');
|
||||
jest.mock('fs');
|
||||
jest.mock('@fluxerjs/core');
|
||||
|
||||
const {messageHelper} = require("../../src/helpers/messageHelper.js");
|
||||
|
||||
describe('messageHelper', () => {
|
||||
|
||||
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"},
|
||||
{name: "someSecondPerson", proxy: undefined},
|
||||
{name: "someOtherPerson", proxy: "?text}"},
|
||||
{name: "someLastPerson", proxy: "{text}"},
|
||||
{name: "someEmojiPerson", proxy: "⭐text"},
|
||||
{name: "someSpacePerson", proxy: "! text"},
|
||||
]
|
||||
|
||||
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', 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[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],
|
||||
['2', '--hello', attachmentUrl,undefined],
|
||||
['3', 'hello', null, {}],
|
||||
['3', '--hello', null, {}],
|
||||
['3', 'hello', attachmentUrl, {}],
|
||||
['3', '--hello', attachmentUrl,{}],
|
||||
])('ID %s with string %s returns correct proxy', async(specificAuthorId, content, attachmentUrl, expected) => {
|
||||
// Act
|
||||
return messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => {
|
||||
// Assert
|
||||
expect(res).toEqual(expected);
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
describe('returnBufferFromText', () => {
|
||||
const charas2000 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
test('returns truncated text and buffer file when text is more than 2000 characters', () => {
|
||||
// Arrange
|
||||
|
||||
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(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
})
|
||||
270
tests/helpers/webhookHelper.test.js
Normal file
270
tests/helpers/webhookHelper.test.js
Normal file
@@ -0,0 +1,270 @@
|
||||
jest.mock('../../src/helpers/messageHelper.js')
|
||||
|
||||
const {messageHelper} = require("../../src/helpers/messageHelper.js");
|
||||
|
||||
jest.mock('../../src/helpers/messageHelper.js', () => {
|
||||
return {messageHelper: {
|
||||
parseProxyTags: jest.fn(),
|
||||
returnBuffer: jest.fn(),
|
||||
returnBufferFromText: jest.fn(),
|
||||
}}
|
||||
})
|
||||
|
||||
const {webhookHelper} = require("../../src/helpers/webhookHelper.js");
|
||||
const {enums} = require("../../src/enums");
|
||||
|
||||
describe('webhookHelper', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
})
|
||||
|
||||
describe(`sendMessageAsMember`, () => {
|
||||
const client = {};
|
||||
const content = "hi"
|
||||
const attachments = {
|
||||
size: 0,
|
||||
first: () => {},
|
||||
foreach: jest.fn()
|
||||
}
|
||||
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);
|
||||
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
|
||||
})
|
||||
})
|
||||
|
||||
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));
|
||||
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
|
||||
})
|
||||
})
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: flaky for some reason
|
||||
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`, () => {
|
||||
|
||||
})
|
||||
|
||||
describe(`getWebhook`, () => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
})
|
||||
Reference in New Issue
Block a user