29 Commits

Author SHA1 Message Date
07f31b5cb4 Merge pull request 'Added workflow_dispatch' (#1) from sync-on-demand into main
All checks were successful
nodeJS remote worker / build (push) Successful in 42s
Reviewed-on: #1
2026-03-03 15:40:20 +11:00
8ec327a149 Added workflow_dispatch
All checks were successful
nodeJS remote worker / build (pull_request) Successful in 48s
This allows one to run the workflow when they want

Signed-off-by: Aster <code@asterfialla.com>
2026-03-03 04:25:01 +11:00
ce57d15b29 Update .gitea/workflows/build-dev.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 45s
2026-03-02 15:01:33 +11:00
2114362dbb Update .gitea/workflows/sync-from-mirror.yaml
All checks were successful
nodeJS remote worker / build (push) Successful in 50s
2026-03-02 14:59:57 +11:00
d1c1754212 Update .gitea/workflows/build-dev.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 53s
2026-03-02 14:53:34 +11:00
7d438b1492 Update .gitea/workflows/sync-from-mirror.yaml
All checks were successful
nodeJS remote worker / build (push) Successful in 43s
2026-02-28 17:50:17 +11:00
c16b397cfa Add .gitea/workflows/sync-from-mirror.yaml
Some checks failed
nodeJS remote worker / build (push) Has been cancelled
Auto-Sync from Mirror / sync (push) Failing after 6s
2026-02-28 17:36:49 +11:00
e862d7c178 Update compose.yaml
All checks were successful
nodeJS remote worker / build (push) Successful in 46s
2026-02-28 17:21:56 +11:00
e40c1266c2 Update .gitea/workflows/build-main.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 45s
2026-02-28 17:20:30 +11:00
b8e155bcc5 Update .gitea/workflows/build-main.yml
Some checks failed
nodeJS remote worker / build (push) Failing after 43s
2026-02-28 17:16:53 +11:00
2551ab4343 Update .gitea/workflows/build-dev.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 47s
2026-02-28 17:16:30 +11:00
135962267d Update .gitea/workflows/build-dev.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 44s
2026-02-28 17:08:18 +11:00
24802f1b75 Update .gitea/workflows/build-main.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 42s
2026-02-28 17:01:19 +11:00
dbdb9fc38c Update .gitea/workflows/build-dev.yml
Some checks failed
nodeJS remote worker / build (push) Has been cancelled
2026-02-28 17:00:56 +11:00
a3e0aa73b4 Update .gitea/workflows/build-dev.yml
Some checks failed
nodeJS remote worker / build (push) Failing after 43s
2026-02-27 13:26:23 +11:00
690344934b merge upstream
All checks were successful
nodeJS remote worker / build (push) Successful in 45s
2026-02-27 12:31:38 +11:00
bf4f55c91b Upload files to ".gitea/workflows"
All checks were successful
nodeJS remote worker / build (push) Successful in 46s
2026-02-26 14:36:36 +11:00
b3566010a2 Update .gitea/workflows/build-main.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 47s
2026-02-26 00:28:41 +11:00
0eee2988ce update build-main with SSH compose deploy
Some checks failed
nodeJS remote worker / build (push) Failing after 44s
2026-02-26 00:20:49 +11:00
a86260cc4a Update .gitea/workflows/build-main.yml
All checks were successful
nodeJS remote worker / build (push) Successful in 45s
2026-02-26 00:15:15 +11:00
506e3ef9dd Update .gitea/workflows/build-main.yml
Some checks failed
nodeJS remote worker / build (push) Failing after 18s
2026-02-26 00:12:08 +11:00
6a33fb592a Update .gitea/workflows/build-main.yml
Some checks failed
nodeJS remote worker / build (push) Has been cancelled
2026-02-26 00:11:21 +11:00
2f255cefd1 Update .gitea/workflows/build-main.yml
Some checks failed
nodeJS remote worker / build (push) Failing after 18s
2026-02-26 00:09:48 +11:00
c54016de77 Delete .gitea/workflows/test-workflow.yaml
Some checks failed
nodeJS remote worker / build (push) Failing after 19s
2026-02-26 00:08:03 +11:00
31da15eaeb Update .gitea/workflows/build.yml
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
nodeJS remote worker / build (push) Failing after 19s
2026-02-26 00:07:40 +11:00
43f4302dbc add build workflow
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
nodeJS remote worker / build (push) Failing after 33s
2026-02-25 23:55:42 +11:00
af3da44946 test
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 41s
2026-02-25 23:34:29 +11:00
af57e2e6a3 add workflows 2026-02-25 23:21:13 +11:00
77191566e3 initial setup 2026-02-25 23:18:48 +11:00
30 changed files with 799 additions and 2016 deletions

View File

@@ -6,8 +6,6 @@ on:
pull_request:
branches: ["develop", "Develop"]
workflow_dispatch:
jobs:
build:
@@ -45,5 +43,4 @@ jobs:
port: 22
script: |
cd ${{ secrets.BOT_DIRECTORY }}
docker compose pull
docker compose up -d pluralflux-dev

View File

@@ -6,8 +6,6 @@ on:
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
build:
@@ -45,5 +43,4 @@ jobs:
port: 22
script: |
cd ${{ secrets.BOT_DIRECTORY }}
docker compose pull
docker compose up -d pluralflux-prod

9
.gitignore vendored
View File

@@ -1,13 +1,8 @@
node_modules/
build/
tmp/
temp/
node_modules
.idea
secrets/
coverage
config.json
coverage
log.txt
.env
oya.png
variables.env
.env.production

View File

@@ -7,4 +7,4 @@ FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["npm", "start"]
CMD ["node", "src/bot.js"]

View File

@@ -7,6 +7,8 @@ PluralFlux is a proxybot akin to PluralKit and Tupperbox, but for [Fluxer](https
[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.

View File

@@ -1,26 +1,59 @@
services:
main:
image: engineering.sanya.gay/pluralflux/pluralflux
container_name: pluralflux
pluralflux-dev:
image: engineering.sanya.gay/pluralflux/pluralflux-dev:latest
container_name: pluralflux-dev
restart: unless-stopped
networks:
- pluralflux-dev-net
env_file: "variables.env"
postgres:
pluralflux-prod:
image: engineering.sanya.gay/pluralflux/pluralflux:latest
container_name: pluralflux-prod
restart: unless-stopped
networks:
- pluralflux-net
env_file: "variables-prod.env"
postgres-dev:
image: postgres:latest
container_name: pluralflux-dev-postgres
env_file: "variables.env"
volumes:
- pgdata:/var/lib/postgresql
- pgdataDev:/var/lib/postgresql
- ./pgBackup:/mnt/pgBackup
ports:
- "5432:5432"
networks:
- pluralflux-dev-net
postgres-prod:
image: postgres:latest
container_name: pluralflux-prod-postgres
env_file: "variables-prod.env"
volumes:
- pgdata:/var/lib/postgresql
- ./pgBackup/prod/:/mnt/pgBackup/prod
networks:
- pluralflux-net
pgadmin:
image: dpage/pgadmin4:latest
container_name: pluralflux-pgadmin
ports:
- "5050:80"
env_file: "variables.env"
depends_on:
- postgres
- postgres-dev
- postgres-prod
networks:
- pluralflux-net
- pluralflux-dev-net
volumes:
- pgadmindata:/var/lib/pgadmin
- ./pgBackup:/mnt/host
# uncomment the above line if you plan to restore / backup dump files from PGAdmin UI
networks:
pluralflux-net:
driver: bridge
pluralflux-dev-net:
driver: bridge
volumes:
pgdata:
pgdataDev:
pgadmindata:
pgdata:

View File

@@ -1,26 +0,0 @@
import "reflect-metadata"
import { DataSource } from "typeorm"
import * as env from 'dotenv';
import * as path from "path";
env.config();
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.POSTGRES_ENDPOINT,
port: 5432,
username: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "postgres",
synchronize: false,
logging: false,
entities: [path.join(__dirname, "./entity/*.{ts,js}")],
migrations: [path.join(__dirname, "./migrations/*.{ts,js}")],
migrationsRun: true,
migrationsTableName: 'migrations',
migrationsTransactionMode: 'all',
invalidWhereValuesBehavior: {
null: "sql-null",
undefined: "throw",
},
});

View File

@@ -1,40 +0,0 @@
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Unique} from "typeorm"
@Entity({name: "Member", synchronize: true})
@Unique("UQ_Member_userid_name", ['userid', 'name'])
export class Member {
@PrimaryGeneratedColumn()
id: number
@Column()
userid: string
@Column({
length: 100
})
name: string
@Column({
type: "varchar",
nullable: true,
length: 100
})
displayname: string
@Column({
nullable: true,
})
proxy: string
@Column({
nullable: true,
})
propic: string
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
}

View File

@@ -1,14 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Update1772417745487 implements MigrationInterface {
name = 'Update1772417745487'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "Member" ("id" SERIAL NOT NULL, "userid" character varying NOT NULL, "name" character varying(100) NOT NULL, "displayname" character varying(100), "proxy" character varying, "propic" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_235428a1d87c5f639ef7b7cf170" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "Member"`);
}
}

View File

@@ -1,12 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddData1772419448503 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`INSERT INTO "Member"(id, userid, name,displayname, proxy, propic, "createdAt", "updatedAt") SELECT id,userid, name,displayname, proxy, propic, "createdAt", "updatedAt" FROM "Members";`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`TRUNCATE TABLE "Member"`);
}
}

View File

@@ -1,17 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class DeleteDuplicates1772825438973 implements MigrationInterface {
name= "DeleteDuplicates1772825438973"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE
FROM "Member" a USING "Member" b
WHERE a.id
> b.id
AND a.name = b.name
AND a.userid = b.userid;`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@@ -1,14 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Update1772830252670 implements MigrationInterface {
name = 'Update1772830252670'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Member" ADD CONSTRAINT "UQ_Member_userid_name" UNIQUE ("userid", "name")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Member" DROP CONSTRAINT "UQ_Member_userid_name"`);
}
}

1957
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,36 +7,26 @@
"type": "git",
"url": "https://github.com/pieartsy/PluralFlux.git"
},
"type": "commonjs",
"private": true,
"dependencies": {
"@fluxerjs/core": "^1.2.2",
"dotenv": "^17.3.1",
"pg": "^8.19.0",
"pg": "^8.18.0",
"pg-hstore": "^2.3.4",
"pm2": "^6.0.14",
"psql": "^0.0.1",
"reflect-metadata": "^0.2.2",
"tmp": "^0.2.5",
"typeorm": "^0.3.28"
"sequelize": "^6.37.7",
"tmp": "^0.2.5"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@fetch-mock/jest": "^0.2.20",
"@types/node": "^25.3.3",
"babel-jest": "^30.2.0",
"fetch-mock": "^12.6.0",
"jest": "^30.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
"jest": "^30.2.0"
},
"scripts": {
"test": "jest",
"start": "ts-node src/bot.js",
"new-migration": "typeorm-ts-node-commonjs migration:create database/migrations/update",
"generate-db": "typeorm-ts-node-commonjs migration:generate -d database/data-source.ts database/migrations/update",
"run-migration": "typeorm-ts-node-commonjs migration:run -d database/data-source.ts"
"test": "jest"
}
}

6
secrets.env Normal file
View File

@@ -0,0 +1,6 @@
FLUXER_BOT_TOKEN=<your bot token here>
POSTGRES_PASSWORD=<your postgres password here>
PGADMIN_DEFAULT_EMAIL: <default postgres admin login>
PGADMIN_DEFAULT_PASSWORD: <your postgres password here>
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'

View File

@@ -1,28 +1,24 @@
const {Client, Events, Message} = require('@fluxerjs/core');
const {messageHelper} = require("./helpers/messageHelper.js");
const {enums} = require("./enums.js");
const {commands} = require("./commands.js");
const {webhookHelper} = require("./helpers/webhookHelper.js");
const env = require('dotenv');
const {utils} = require("./helpers/utils.js");
const { AppDataSource } = require("../database/data-source");
import { Client, Events, Message } 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 env from 'dotenv';
import {utils} from "./helpers/utils.js";
env.config();
env.config({path: './.env'});
const token = process.env.FLUXER_BOT_TOKEN;
const debug = process.env.debug;
if (!token) {
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
process.exit(1);
}
client = new Client({ intents: 0 });
module.exports.client = client;
export const client = new Client({ intents: 0 });
client.on(Events.MessageCreate, async (message) => {
await module.exports.handleMessageCreate(message);
await handleMessageCreate(message);
});
/**
@@ -32,7 +28,7 @@ client.on(Events.MessageCreate, async (message) => {
* @param {Message} message - The message object
*
**/
module.exports.handleMessageCreate = async function(message) {
export const handleMessageCreate = async function(message) {
try {
// Ignore bots
if (message.author.bot) return;
@@ -64,15 +60,12 @@ module.exports.handleMessageCreate = async function(message) {
}
}
catch(error) {
if(debug){console.error("An error occurred at unix timestamp " + Date.now() + "while processing the command: " + message + " with error:" + error);}
else{console.error(error);}
process.exit(2); //need this for now just to make sure the bot continues to restart on errors, since it would seem that fluxer.js doesn't define custom error types. TODO: map out some exit codes
console.error(error);
}
}
client.on(Events.Ready, () => {
console.log(`Logged in as ${client.user?.username}`);
if(debug){console.log(Date.now() + `: Currently running in debug mode!`)}
});
let guildCount = 0;
@@ -86,14 +79,12 @@ function printGuilds() {
}
const debouncePrintGuilds = utils.debounce(printGuilds, 2000);
// export const debounceLogin = utils.debounce(client.login, 60000);
export const debounceLogin = utils.debounce(client.login, 60000);
module.exports.login = async function() {
export const login = async function() {
try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
await client.login(token);
// await db.check_connection();
} catch (err) {
console.error('Login failed:', err);
process.exit(1);
@@ -102,7 +93,7 @@ module.exports.login = async function() {
function main()
{
exports.login();
login();
}
main();

View File

@@ -1,20 +1,20 @@
const {messageHelper} = require("./helpers/messageHelper.js");
const {enums} = require("./enums.js");
const {memberHelper} = require("./helpers/memberHelper.js");
const {EmbedBuilder} = require("@fluxerjs/core");
const {importHelper} = require("./helpers/importHelper.js");
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 commands = {
const cmds = {
commandsMap: new Map(),
aliasesMap: new Map()
};
commands.aliasesMap.set('m', {command: 'member'})
cmds.aliasesMap.set('m', {command: 'member'})
commands.commandsMap.set('member', {
cmds.commandsMap.set('member', {
description: enums.help.SHORT_DESC_MEMBER,
async execute(message, args) {
await commands.memberCommand(message, args)
await cmds.memberCommand(message, args)
}
})
@@ -26,7 +26,7 @@ commands.commandsMap.set('member', {
* @param {string[]} args - The parsed arguments
*
**/
commands.memberCommand = async function (message, args) {
cmds.memberCommand = async function (message, 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;
@@ -53,10 +53,10 @@ commands.memberCommand = async function (message, args) {
}
commands.commandsMap.set('help', {
cmds.commandsMap.set('help', {
description: enums.help.SHORT_DESC_HELP,
async execute(message) {
const fields = [...commands.commandsMap.entries()].map(([name, cmd]) => ({
const fields = [...cmds.commandsMap.entries()].map(([name, cmd]) => ({
name: `${messageHelper.prefix}${name}`,
value: cmd.description,
inline: true,
@@ -73,10 +73,10 @@ commands.commandsMap.set('help', {
},
})
commands.commandsMap.set('import', {
cmds.commandsMap.set('import', {
description: enums.help.SHORT_DESC_IMPORT,
async execute(message, args) {
await commands.importCommand(message, args);
await cmds.importCommand(message, args);
}
})
@@ -88,7 +88,7 @@ commands.commandsMap.set('import', {
* @param {string[]} args - The parsed arguments
*
**/
commands.importCommand = async function (message, args) {
cmds.importCommand = async function (message, 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);
@@ -119,4 +119,4 @@ commands.importCommand = async function (message, args) {
}
module.exports.commands = commands;
export const commands = cmds;

86
src/database.js Normal file
View File

@@ -0,0 +1,86 @@
import {DataTypes, Sequelize} from 'sequelize';
import * as env from 'dotenv';
env.config();
const password = process.env.POSTGRES_PASSWORD;
if (!password) {
console.error("Missing POSTGRES_PASSWORD environment variable.");
process.exit(1);
}
const db = {};
const sequelize = new Sequelize('postgres', 'postgres', password, {
host: 'localhost',
logging: false,
dialect: 'postgres'
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
db.members = sequelize.define('Member', {
userid: {
type: DataTypes.STRING,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
displayname: {
type: DataTypes.STRING,
},
propic: {
type: DataTypes.STRING,
},
proxy: {
type: DataTypes.STRING,
}
});
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 () {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
await syncModels();
} catch (err) {
console.error('Unable to connect to the database:', err);
process.exit(1);
}
}
/**
* Syncs Sequelize models.
*/
async function syncModels() {
try {
await sequelize.sync()
console.log('Models synced successfully.');
} catch(err) {
console.error('Syncing models did not work', err);
process.exit(1);
}
}
export const database = db;

View File

@@ -1,6 +1,6 @@
const enums = {};
const helperEnums = {};
enums.err = {
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.",
@@ -26,7 +26,7 @@ enums.err = {
CANNOT_FETCH_RESOURCE: "Could not download the file at this time."
}
enums.help = {
helperEnums.help = {
SHORT_DESC_HELP: "Lists available commands.",
SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.",
SHORT_DESC_IMPORT: "Imports from PluralKit.",
@@ -43,11 +43,11 @@ enums.help = {
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.**\n\n**PRO TIP**: For privacy reasons, try DMing the bot with this command and your JSON file--it should still work the same."
}
enums.misc = {
helperEnums.misc = {
ATTACHMENT_SENT_BY: "Attachment sent by:",
ATTACHMENT_EXPIRATION_WARNING: "**NOTE:** Because this profile picture is hosted on Fluxer, it will expire. To avoid this, upload the picture to another website like <https://imgbb.com/> and link to it directly.",
FLUXER_ATTACHMENT_URL: "https://fluxerusercontent.com/attachments/"
}
module.exports.enums = enums;
export const enums = helperEnums;

View File

@@ -1,7 +1,7 @@
const {enums} = require("../enums.js");
const {memberHelper} = require("./memberHelper.js");
import {enums} from "../enums.js";
import {memberHelper} from "./memberHelper.js";
const importHelper = {};
const ih = {};
/**
* Tries to import from Pluralkit.
@@ -12,7 +12,7 @@ const importHelper = {};
* @returns {Promise<string>} A successful addition of all members.
* @throws {Error} When the member exists, or creating a member doesn't work.
*/
importHelper.pluralKitImport = async function (authorId, attachmentUrl= null) {
ih.pluralKitImport = async function (authorId, attachmentUrl= null) {
let fetchResult, pkData;
if (!attachmentUrl) {
throw new Error(enums.err.NOT_JSON_FILE);
@@ -55,4 +55,4 @@ importHelper.pluralKitImport = async function (authorId, attachmentUrl= null) {
return aggregatedText;
}
exports.importHelper = importHelper;
export const importHelper = ih;

View File

@@ -1,9 +1,10 @@
const {enums} = require("../enums.js");
const {EmbedBuilder} = require("@fluxerjs/core");
const {utils} = require("./utils.js");
const {memberRepo} = require("../repositories/memberRepo.js");
import {database} from '../database.js';
import {enums} from "../enums.js";
import {Op} from "sequelize";
import {EmbedBuilder} from "@fluxerjs/core";
import {utils} from "./utils.js";
const memberHelper = {};
const mh = {};
const commandList = ['new', 'remove', 'name', 'list', 'displayname', 'proxy', 'propic'];
const newAndRemoveCommands = ['new', 'remove'];
@@ -22,14 +23,14 @@ const newAndRemoveCommands = ['new', 'remove'];
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
* @returns {Promise<{EmbedBuilder, string[], string}>} A member info embed + info/errors.
*/
memberHelper.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) {
mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) {
let memberName, command, isHelp = false;
// checks whether command is in list, otherwise assumes it's a name
// ex: pf;member remove, pf;member remove --help
// ex: pf;member, pf;member --help
if (args.length === 0 || args[0] === '--help' || args[0] === '') {
return memberHelper.getMemberCommandInfo();
return mh.getMemberCommandInfo();
}
// ex: pf;member remove somePerson
if (commandList.includes(args[0])) {
@@ -51,7 +52,7 @@ memberHelper.parseMemberCommand = async function (authorId, authorFull, args, at
isHelp = true;
}
return await memberHelper.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration);
return await mh.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration)
}
/**
@@ -70,18 +71,17 @@ memberHelper.parseMemberCommand = async function (authorId, authorFull, args, at
* @returns {Promise <EmbedBuilder>} A list of 25 members as an embed.
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
* @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors.
* @returns {Promise<string>} - A help message
* @throws {Error} When there's no member or a command is not recognized.
*/
memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) {
mh.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) {
if (!command && !memberName && !isHelp) {
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
}
else if (isHelp) {
return memberHelper.sendHelpEnum(command);
return mh.sendHelpEnum(command);
}
else if (command === "list") {
return await memberHelper.getAllMembersInfo(authorId, authorFull);
return await mh.getAllMembersInfo(authorId, authorFull);
}
else if (!memberName && !isHelp) {
throw new Error(enums.err.NO_MEMBER);
@@ -92,10 +92,10 @@ memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp
// ex: pf;member blah blah
if (command && memberName && (values.length > 0 || newAndRemoveCommands.includes(command) || attachmentUrl)) {
return await memberHelper.memberCommandHandler(authorId, command, memberName, values, attachmentUrl, attachmentExpiration);
return await mh.memberCommandHandler(authorId, command, memberName, values, attachmentUrl, attachmentExpiration);
}
else if (memberName && values.length === 0) {
return await memberHelper.sendCurrentValue(authorId, memberName, command);
return await mh.sendCurrentValue(authorId, memberName, command);
}
}
@@ -112,12 +112,12 @@ memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp
* @returns {Promise<{EmbedBuilder, string[], string}>} A member info embed + info/errors.
* @throws {Error} When there's no member
*/
memberHelper.sendCurrentValue = async function(authorId, memberName, command= null) {
const member = await memberRepo.getMemberByName(authorId, memberName);
mh.sendCurrentValue = async function(authorId, memberName, command= null) {
const member = await mh.getMemberByName(authorId, memberName);
if (!member) throw new Error(enums.err.NO_MEMBER);
if (!command) {
return memberHelper.getMemberInfo(member);
return mh.getMemberInfo(member);
}
switch (command) {
@@ -138,7 +138,7 @@ memberHelper.sendCurrentValue = async function(authorId, memberName, command= nu
* @param {string} command - The command being called.
* @returns {string} - The help text associated with a command.
*/
memberHelper.sendHelpEnum = function(command) {
mh.sendHelpEnum = function(command) {
switch (command) {
case 'new':
return enums.help.NEW;
@@ -167,22 +167,25 @@ memberHelper.sendHelpEnum = function(command) {
* @param {string[]} values - The values to be passed in. Only includes the values after member name and command name.
* @param {string | null} attachmentUrl - The attachment URL, if any
* @param {string | null} attachmentExpiration - The attachment expiry date, if any
* @returns {Promise<string> | Promise <EmbedBuilder> | Promise<{EmbedBuilder, [string], string}>}
* @returns {Promise<string>} A success message.
* @returns {Promise <EmbedBuilder>} A list of 25 members as an embed.
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
* @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors.
*/
memberHelper.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
mh.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
switch (command) {
case 'new':
return await memberHelper.addNewMember(authorId, memberName, values, attachmentUrl, attachmentExpiration);
return await mh.addNewMember(authorId, memberName, values, attachmentUrl, attachmentExpiration);
case 'remove':
return await memberHelper.removeMember(authorId, memberName);
return await mh.removeMember(authorId, memberName);
case 'name':
return await memberHelper.updateName(authorId, memberName, values[0]);
return await mh.updateName(authorId, memberName, values[0]);
case 'displayname':
return await memberHelper.updateDisplayName(authorId, memberName, values[0]);
return await mh.updateDisplayName(authorId, memberName, values[0]);
case 'proxy':
return await memberHelper.updateProxy(authorId, memberName, values[0]);
return await mh.updateProxy(authorId, memberName, values[0]);
case 'propic':
return await memberHelper.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration);
return await mh.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration);
default:
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
}
@@ -199,13 +202,13 @@ memberHelper.memberCommandHandler = async function(authorId, command, memberName
* @param {string | null} [attachmentExpiration] - The attachment expiry date, if any
* @returns {Promise<{EmbedBuilder, string[], string}>} A successful addition.
*/
memberHelper.addNewMember = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
mh.addNewMember = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
const displayName = values[0];
const proxy = values[1];
const propic = values[2] ?? attachmentUrl;
const memberObj = await memberHelper.addFullMember(authorId, memberName, displayName, proxy, propic, attachmentExpiration);
const memberInfoEmbed = memberHelper.getMemberInfo(memberObj.member);
const memberObj = await mh.addFullMember(authorId, memberName, displayName, proxy, propic, attachmentExpiration);
const memberInfoEmbed = mh.getMemberInfo(memberObj.member);
return {embed: memberInfoEmbed, errors: memberObj.errors, success: `${memberName} has been added successfully.`}
}
@@ -219,12 +222,12 @@ memberHelper.addNewMember = async function (authorId, memberName, values, attach
* @returns {Promise<string>} A successful update.
* @throws {RangeError} When the name doesn't exist.
*/
memberHelper.updateName = async function (authorId, memberName, name) {
mh.updateName = async function (authorId, memberName, name) {
const trimmedName = name.trim();
if (trimmedName === '') {
throw new RangeError(`Name ${enums.err.NO_VALUE}`);
}
return await memberHelper.updateMemberField(authorId, memberName, "name", trimmedName);
return await mh.updateMemberField(authorId, memberName, "name", trimmedName);
}
/**
@@ -237,7 +240,7 @@ memberHelper.updateName = async function (authorId, memberName, name) {
* @returns {Promise<string>} A successful update.
* @throws {RangeError} When the display name is too long or doesn't exist.
*/
memberHelper.updateDisplayName = async function (authorId, membername, displayname) {
mh.updateDisplayName = async function (authorId, membername, displayname) {
const trimmedName = displayname.trim();
if (trimmedName.length > 32) {
@@ -246,7 +249,7 @@ memberHelper.updateDisplayName = async function (authorId, membername, displayna
else if (trimmedName === '') {
throw new RangeError(`Display name ${enums.err.NO_VALUE}`);
}
return await memberHelper.updateMemberField(authorId, membername, "displayname", trimmedName);
return await mh.updateMemberField(authorId, membername, "displayname", trimmedName);
}
/**
@@ -258,11 +261,11 @@ memberHelper.updateDisplayName = async function (authorId, membername, displayna
* @param {string} proxy - The proxy to set
* @returns {Promise<string> } A successful update.
*/
memberHelper.updateProxy = async function (authorId, memberName, proxy) {
mh.updateProxy = async function (authorId, memberName, proxy) {
// Throws error if exists
await memberHelper.checkIfProxyExists(authorId, proxy);
await mh.checkIfProxyExists(authorId, proxy);
return await memberHelper.updateMemberField(authorId, memberName, "proxy", proxy);
return await mh.updateMemberField(authorId, memberName, "proxy", proxy);
}
/**
@@ -276,12 +279,12 @@ memberHelper.updateProxy = async function (authorId, memberName, proxy) {
* @param {string | null} attachmentExpiration - The attachment expiry date, if any
* @returns {Promise<string>} A successful update.
*/
memberHelper.updatePropic = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
mh.updatePropic = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
const imgUrl = values ?? attachmentUrl;
// Throws error if invalid
await utils.checkImageFormatValidity(imgUrl);
const expirationWarning = utils.setExpirationWarning(imgUrl, attachmentExpiration);
return await memberHelper.updateMemberField(authorId, memberName, "propic", imgUrl, expirationWarning);
return await mh.updateMemberField(authorId, memberName, "propic", imgUrl, expirationWarning);
}
/**
@@ -293,8 +296,13 @@ memberHelper.updatePropic = async function (authorId, memberName, values, attach
* @returns {Promise<string>} A successful removal.
* @throws {Error} When there is no member to remove.
*/
memberHelper.removeMember = async function (authorId, memberName) {
const destroyed = await memberRepo.removeMember(authorId, memberName);
mh.removeMember = async function (authorId, memberName) {
const destroyed = await database.members.destroy({
where: {
name: {[Op.iLike]: memberName},
userid: authorId
}
})
if (destroyed > 0) {
return `Member "${memberName}" has been deleted.`;
} else {
@@ -314,11 +322,11 @@ memberHelper.removeMember = async function (authorId, memberName) {
* @param {string | null} [proxy] - The proxy tag of the member.
* @param {string | null} [propic] - The profile picture URL of the member.
* @param {string | null} [attachmentExpiration] - The expiration date of an uploaded profile picture.
* @returns {Promise<{Members, string[]}>} A successful addition object, including errors if there are any.
* @returns {Promise<{model, string[]}>} 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.
*/
memberHelper.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) {
const existingMember = await memberRepo.getMemberByName(authorId, memberName);
mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) {
const existingMember = await mh.getMemberByName(authorId, memberName);
if (existingMember) {
throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`);
}
@@ -348,7 +356,7 @@ memberHelper.addFullMember = async function (authorId, memberName, displayName =
let isValidProxy;
if (proxy && proxy.length > 0) {
try {
const proxyExists = await memberHelper.checkIfProxyExists(authorId, proxy);
const proxyExists = await mh.checkIfProxyExists(authorId, proxy);
isValidProxy = !proxyExists;
}
catch(e) {
@@ -357,22 +365,21 @@ memberHelper.addFullMember = async function (authorId, memberName, displayName =
}
}
let isValidPropic, expirationWarning;
let isValidPropic;
if (propic && propic.length > 0) {
try {
isValidPropic = await utils.checkImageFormatValidity(propic);
expirationWarning = utils.setExpirationWarning(propic, attachmentExpiration);
if (expirationWarning) {
errors.push(expirationWarning);
}
}
catch(e) {
errors.push(`Tried to set profile picture to \"${propic}\". ${e.message}. ${enums.err.SET_TO_NULL}`);
isValidPropic = false;
}
}
const member = await memberRepo.createMember({
const expirationWarning = utils.setExpirationWarning(propic, attachmentExpiration);
if (expirationWarning) {
errors.push(expirationWarning);
}
const member = await database.members.create({
name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
});
@@ -391,9 +398,14 @@ memberHelper.addFullMember = async function (authorId, memberName, displayName =
* @returns {Promise<string>} A successful update.
* @throws {Error} When no member row was updated.
*/
memberHelper.updateMemberField = async function (authorId, memberName, columnName, value, expirationWarning = null) {
const res = await memberRepo.updateMemberField(authorId, memberName, columnName, value);
if (res === 0) {
mh.updateMemberField = async function (authorId, memberName, columnName, value, expirationWarning = null) {
const res = await database.members.update({[columnName]: value}, {
where: {
name: {[Op.iLike]: memberName},
userid: authorId
}
})
if (res[0] === 0) {
throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
} else {
return `Updated ${columnName} for ${memberName} to ${value}${expirationWarning ? `. ${expirationWarning}.` : '.'}`;
@@ -403,10 +415,10 @@ memberHelper.updateMemberField = async function (authorId, memberName, columnNam
/**
* Gets the details for a member.
*
* @param {{Member, string[]}} member - The member object
* @param {model} member - The member object
* @returns {EmbedBuilder} The member's info.
*/
memberHelper.getMemberInfo = function (member) {
mh.getMemberInfo = function (member) {
return new EmbedBuilder()
.setTitle(member.name)
.setDescription(`Details for ${member.name}`)
@@ -427,8 +439,8 @@ memberHelper.getMemberInfo = function (member) {
* @returns {Promise<EmbedBuilder>} The info for all members.
* @throws {Error} When there are no members for an author.
*/
memberHelper.getAllMembersInfo = async function (authorId, authorName) {
const members = await memberRepo.getMembersByAuthor(authorId);
mh.getAllMembersInfo = async function (authorId, authorName) {
const members = await mh.getMembersByAuthor(authorId);
if (members.length === 0) throw Error(enums.err.USER_NO_MEMBERS);
const fields = [...members.entries()].map(([index, member]) => ({
name: member.name, value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, inline: true,
@@ -438,6 +450,29 @@ memberHelper.getAllMembersInfo = async function (authorId, 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.
*/
mh.getMemberByName = async function (authorId, memberName) {
return await database.members.findOne({where: {userid: authorId, name: {[Op.iLike]: memberName}}});
}
/**
* 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.
*
@@ -446,12 +481,12 @@ memberHelper.getAllMembersInfo = async function (authorId, authorName) {
* @returns {Promise<boolean> } Whether the proxy exists.
* @throws {Error} When an empty proxy was provided, or no proxy exists.
*/
memberHelper.checkIfProxyExists = async function (authorId, proxy) {
mh.checkIfProxyExists = async function (authorId, 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);
const memberList = await memberRepo.getMembersByAuthor(authorId);
const memberList = await mh.getMembersByAuthor(authorId);
const proxyExists = memberList.some(member => member.proxy === proxy);
if (proxyExists) {
throw new Error(enums.err.PROXY_EXISTS);
@@ -464,7 +499,7 @@ memberHelper.checkIfProxyExists = async function (authorId, proxy) {
*
* @returns {EmbedBuilder } An embed of member commands.
*/
memberHelper.getMemberCommandInfo = function() {
mh.getMemberCommandInfo = function() {
const fields = [
{name: `**new**`, value: enums.help.NEW, inline: false},
{name: `**remove**`, value: enums.help.REMOVE, inline: false},
@@ -481,4 +516,4 @@ memberHelper.getMemberCommandInfo = function() {
}
module.exports.memberHelper = memberHelper;
export const memberHelper = mh;

View File

@@ -1,4 +1,4 @@
const {memberRepo} = require('../repositories/memberRepo.js');
import {memberHelper} from "./memberHelper.js";
const msgh = {};
@@ -39,7 +39,7 @@ msgh.parseCommandArgs = function(content, commandName) {
* @returns {Promise<{model, string, bool}>} The proxy message object.
*/
msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){
const members = await memberRepo.getMembersByAuthor(authorId);
const members = await memberHelper.getMembersByAuthor(authorId);
// If an author has no members, no sense in searching for proxy
if (members.length === 0) {
return;
@@ -80,4 +80,4 @@ msgh.returnBufferFromText = function (text) {
return {text: text, file: undefined}
}
module.exports.messageHelper = msgh;
export const messageHelper = msgh;

View File

@@ -1,8 +1,8 @@
const {enums} = require('../enums');
import {enums} from '../enums.js'
const utils = {};
const u = {};
utils.debounce = function(func, delay) {
u.debounce = function(func, delay) {
let timeout = null;
return function (...args) {
clearTimeout(timeout);
@@ -18,7 +18,7 @@ utils.debounce = function(func, delay) {
* @returns {bool} - Whether the image is in a valid format
* @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements.
*/
utils.checkImageFormatValidity = async function (imageUrl) {
u.checkImageFormatValidity = async function (imageUrl) {
const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp'];
let response, blobFile;
try {
@@ -41,7 +41,7 @@ utils.checkImageFormatValidity = async function (imageUrl) {
* @param {string | null} [expirationString] - An expiration date string.
* @returns {string | null} A description of the expiration, or null.
*/
utils.setExpirationWarning = function (imgUrl = null, expirationString = null) {
u.setExpirationWarning = function (imgUrl = null, expirationString = null) {
if (imgUrl && imgUrl.startsWith(enums.misc.FLUXER_ATTACHMENT_URL)) {
return enums.misc.ATTACHMENT_EXPIRATION_WARNING;
}
@@ -54,4 +54,4 @@ utils.setExpirationWarning = function (imgUrl = null, expirationString = null) {
return null;
}
module.exports.utils = utils;
export const utils = u;

View File

@@ -1,8 +1,8 @@
const {messageHelper} = require("./messageHelper.js");
const {Webhook, Channel, Message, Client} = require('@fluxerjs/core');
const {enums} = require("../enums.js");
import {messageHelper} from "./messageHelper.js";
import {Webhook, Channel, Message, Client} from '@fluxerjs/core';
import {enums} from "../enums.js";
const webhookHelper = {};
const wh = {};
const name = 'PluralFlux Proxy Webhook';
@@ -13,7 +13,7 @@ const name = 'PluralFlux Proxy Webhook';
* @param {Message} message - The full message object.
* @throws {Error} When the proxy message is not in a server.
*/
webhookHelper.sendMessageAsMember = async function(client, message) {
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);
// If the message doesn't match a proxy, just return.
@@ -27,7 +27,7 @@ webhookHelper.sendMessageAsMember = async function(client, message) {
if (proxyMatch.hasAttachment) {
return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname ?? proxyMatch.member.name}`)
}
await webhookHelper.replaceMessage(client, message, proxyMatch.message, proxyMatch.member);
await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member);
}
/**
@@ -39,11 +39,11 @@ webhookHelper.sendMessageAsMember = async function(client, message) {
* @param {model} member - A member object from the database.
* @throws {Error} When there's no message to send.
*/
webhookHelper.replaceMessage = async function(client, message, text, member) {
wh.replaceMessage = async function(client, message, text, member) {
// attachment logic is not relevant yet, text length will always be over 0 right now
if (text.length > 0 || message.attachments.size > 0) {
const channel = client.channels.get(message.channelId);
const webhook = await webhookHelper.getOrCreateWebhook(client, channel);
const webhook = await wh.getOrCreateWebhook(client, channel);
const username = member.displayname ?? member.name;
if (text.length <= 2000) {
await webhook.send({content: text, username: username, avatar_url: member.propic})
@@ -68,10 +68,10 @@ webhookHelper.replaceMessage = async function(client, message, text, member) {
* @returns {Webhook} A webhook object.
* @throws {Error} When no webhooks are allowed in the channel.
*/
webhookHelper.getOrCreateWebhook = async function(client, channel) {
wh.getOrCreateWebhook = async function(client, channel) {
// If channel doesn't allow webhooks
if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED);
let webhook = await webhookHelper.getWebhook(client, channel)
let webhook = await wh.getWebhook(client, channel)
if (!webhook) {
webhook = await channel.createWebhook({name: name});
}
@@ -85,7 +85,7 @@ webhookHelper.getOrCreateWebhook = async function(client, channel) {
* @param {Channel} channel - The channel the message was sent in.
* @returns {Webhook} A webhook object.
*/
webhookHelper.getWebhook = async function(client, channel) {
wh.getWebhook = async function(client, channel) {
const channelWebhooks = await channel?.fetchWebhooks() ?? [];
if (channelWebhooks.length === 0) {
return;
@@ -93,4 +93,4 @@ webhookHelper.getWebhook = async function(client, channel) {
return channelWebhooks.find((webhook) => webhook.name === name);
}
module.exports.webhookHelper = webhookHelper;
export const webhookHelper = wh;

View File

@@ -1,74 +0,0 @@
const Member = require("../../database/entity/Member");
const { AppDataSource } = require("../../database/data-source");
const {ILike} = require("typeorm");
const members = AppDataSource.getRepository(Member.Member)
const memberRepo = {};
/**
* 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<Member | null>} The member object or null if not found.
*/
memberRepo.getMemberByName = async function (authorId, memberName) {
return await members.findOne({where: {userid: authorId, name: ILike(memberName)}});
}
/**
* Gets all members belonging to the author.
*
* @async
* @param {string} authorId - The author of the message
* @returns {Promise<Member[]>} The member object array.
*/
memberRepo.getMembersByAuthor = async function (authorId) {
return await members.findBy({userid: authorId});
}
/**
* Removes a member.
*
* @async
* @param {string} authorId - The author of the message
* @param {string} memberName - The name of the member to remove
* @returns {Promise<number>} Number of results removed.
*/
memberRepo.removeMember = async function (authorId, memberName) {
const deleted = await members.delete({ name: ILike(memberName), userid: authorId })
return deleted.affected;
}
/**
* Adds a member with full details.
*
* @async
* @param {{name: string, userid: string, displayname: (string|null), proxy: (string|null), propic: (string|null)}} createObj - Object with parameters in it
* @returns {Promise<Member>} A successful inserted object.
*/
memberRepo.createMember = async function (createObj) {
return await members.save({
name: createObj.name, userid: createObj.userid, displayname: createObj.displayname, proxy: createObj.proxy, propic: createObj.propic
});
}
/**
* Updates one fields for a member in the database.
*
* @async
* @param {string} authorId - The author of the message
* @param {string} memberName - The member to update
* @param {string} columnName - The column name to update.
* @param {string} value - The value to update to.
* @returns {Promise<number>} A successful update.
*/
memberRepo.updateMemberField = async function (authorId, memberName, columnName, value) {
const updated = await members.update({
name: ILike(memberName),
userid: authorId
}, {[columnName]: value})
return updated.affected;
}
module.exports.memberRepo = memberRepo;

View File

@@ -56,15 +56,6 @@ jest.mock("../src/commands.js", () => {
}
})
jest.mock('../database/data-source.ts', () => {
return {
AppDataSource: {
isInitialized: false,
initialize: jest.fn().mockResolvedValue()
}
}
})
const {Client, Events} = require('@fluxerjs/core');
const {messageHelper} = require("../src/helpers/messageHelper.js");

View File

@@ -2,15 +2,17 @@ const {enums} = require('../../src/enums.js');
const {utils} = require("../../src/helpers/utils.js");
jest.mock('@fluxerjs/core', () => jest.fn());
jest.mock('../../src/repositories/memberRepo.js', () => {
jest.mock('../../src/database.js', () => {
return {
memberRepo: {
getMemberByName: jest.fn().mockResolvedValue(),
getMembersByAuthor: jest.fn().mockResolvedValue(),
removeMember: jest.fn().mockResolvedValue(),
createMember: jest.fn().mockResolvedValue(),
updateMemberField: jest.fn().mockResolvedValue(),
database: {
members: {
create: jest.fn().mockResolvedValue(),
update: jest.fn().mockResolvedValue(),
destroy: jest.fn().mockResolvedValue(),
findOne: jest.fn().mockResolvedValue(),
findAll: jest.fn().mockResolvedValue(),
}
}
}
});
@@ -23,8 +25,10 @@ jest.mock("../../src/helpers/utils.js", () => {
}
});
const {Op} = require('sequelize');
const {memberHelper} = require("../../src/helpers/memberHelper.js");
const {memberRepo} = require("../../src/repositories/memberRepo.js");
const {database} = require("../../src/database");
describe('MemberHelper', () => {
const authorId = "0001";
@@ -266,29 +270,29 @@ describe('MemberHelper', () => {
['propic', `The profile picture for ${mockMember.name} is \"${mockMember.propic}\".`],
])('%s calls getMemberByName and returns value', async (command, expected) => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue(mockMember);
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember);
// Act
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command);
// Assert
expect(result).toEqual(expected);
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
})
test('returns error if no member found', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue(null);
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(null);
// Act
await expect(memberHelper.sendCurrentValue(authorId, mockMember.name, 'name')).rejects.toThrow(enums.err.NO_MEMBER);
// Assert
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
});
test('calls getMemberInfo with member if no command present', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue(mockMember);
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember);
jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue('member info');
// Act
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, null);
@@ -305,13 +309,13 @@ describe('MemberHelper', () => {
])('returns null message if no value found', async (command, expected) => {
// Arrange
const empty = {name: mockMember.name, displayname: null, proxy: null, propic: null}
memberRepo.getMemberByName.mockResolvedValue(empty);
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(empty);
// Act
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command);
// Assert
expect(result).toEqual(expected);
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
})
})
@@ -475,38 +479,39 @@ describe('MemberHelper', () => {
})
describe('addFullMember', () => {
const {database} = require('../../src/database.js');
beforeEach(() => {
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue();
})
test('calls getMemberByName', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue();
// Act
await memberHelper.addFullMember(authorId, mockMember.name)
// Assert
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
})
test('if getMemberByName returns member, throw error', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue({name: mockMember.name});
memberHelper.getMemberByName.mockResolvedValue({name: mockMember.name});
// Act & Assert
await expect(memberHelper.addFullMember(authorId, mockMember.name)).rejects.toThrow(`Can't add ${mockMember.name}. ${enums.err.MEMBER_EXISTS}`)
// Assert
expect(memberRepo.createMember).not.toHaveBeenCalled();
expect(database.members.create).not.toHaveBeenCalled();
})
test('if name is not filled out, throw error', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue();
// Act
// Act & Assert
await expect(memberHelper.addFullMember(authorId, " ")).rejects.toThrow(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`);
// Assert
expect(memberRepo.createMember).not.toHaveBeenCalled();
expect(database.members.create).not.toHaveBeenCalled();
})
test('if displayname is over 32 characters, call memberRepo.createMember with null value', async () => {
test('if displayname is over 32 characters, call database.member.create with null value', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue();
memberHelper.getMemberByName.mockResolvedValue();
const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const expectedMemberArgs = {
name: mockMember.name,
@@ -515,7 +520,7 @@ describe('MemberHelper', () => {
proxy: null,
propic: null
}
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {
member: expectedMemberArgs,
errors: [`Tried to set displayname to \"${tooLongDisplayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`]
@@ -525,8 +530,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, tooLongDisplayName, null, null);
// Assert
expect(res).toEqual(expectedReturn);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1);
})
test('if proxy, call checkIfProxyExists', async () => {
@@ -539,7 +544,7 @@ describe('MemberHelper', () => {
proxy: null,
propic: null
}
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {member: expectedMemberArgs, errors: []}
// Act
@@ -548,8 +553,8 @@ describe('MemberHelper', () => {
expect(res).toEqual(expectedReturn);
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy);
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).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 () => {
@@ -562,7 +567,7 @@ describe('MemberHelper', () => {
proxy: null,
propic: null
}
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {
member: expectedMemberArgs,
errors: [`Tried to set proxy to \"${mockMember.proxy}\". error. ${enums.err.SET_TO_NULL}`]
@@ -572,8 +577,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy, null)
// Assert
expect(res).toEqual(expectedReturn);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1);
})
test('if propic, call checkImageFormatValidity', async () => {
@@ -585,8 +590,7 @@ describe('MemberHelper', () => {
proxy: null,
propic: null
}
utils.setExpirationWarning = jest.fn().mockReturnValue();
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {member: expectedMemberArgs, errors: []}
// Act
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic);
@@ -594,8 +598,8 @@ describe('MemberHelper', () => {
expect(res).toEqual(expectedReturn);
expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(mockMember.propic);
expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).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 () => {
@@ -608,7 +612,7 @@ describe('MemberHelper', () => {
proxy: null,
propic: null
}
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {
member: expectedMemberArgs,
errors: [`Tried to set profile picture to \"${mockMember.propic}\". error. ${enums.err.SET_TO_NULL}`]
@@ -617,19 +621,19 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic);
// Assert
expect(res).toEqual(expectedReturn);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1);
})
test('calls setExpirationWarning if attachmentExpiration exists', async () => {
// Arrange
utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true);
utils.setExpirationWarning = jest.fn().mockReturnValue(`${enums.misc.ATTACHMENT_EXPIRATION_WARNING}`);
jest.spyOn(memberHelper, 'setExpirationWarning').mockReturnValue(`${enums.misc.ATTACHMENT_EXPIRATION_WARNING}`);
// Act
await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic, attachmentExpiration)
// Assert
expect(utils.setExpirationWarning).toHaveBeenCalledTimes(1);
expect(utils.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic, attachmentExpiration);
expect(memberHelper.setExpirationWarning).toHaveBeenCalledTimes(1);
expect(memberHelper.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic, attachmentExpiration);
})
test('if all values are valid, call database.members.create', async () => {
@@ -642,24 +646,26 @@ describe('MemberHelper', () => {
proxy: mockMember.proxy,
propic: mockMember.propic
}
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true);
utils.setExpirationWarning = jest.fn().mockReturnValue();
const expectedReturn = {member: expectedMemberArgs, errors: []}
// Act
const res = await memberHelper.addFullMember(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic);
// Assert
expect(res).toEqual(expectedReturn);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1);
})
})
describe('updateMemberField', () => {
const {database} = require('../../src/database.js');
beforeEach(() => {
utils.setExpirationWarning = jest.fn().mockReturnValue(`warning`);
memberRepo.updateMemberField = jest.fn().mockResolvedValue([1]);
jest.spyOn(memberHelper, "setExpirationWarning").mockReturnValue(' warning');
database.members = {
update: jest.fn().mockResolvedValue([1])
};
})
test.each([
@@ -674,13 +680,20 @@ describe('MemberHelper', () => {
const res = await memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration)
// Assert
expect(res).toEqual(expected);
expect(memberRepo.updateMemberField).toHaveBeenCalledTimes(1);
expect(memberRepo.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, columnName, value)
expect(database.members.update).toHaveBeenCalledTimes(1);
expect(database.members.update).toHaveBeenCalledWith({[columnName]: value}, {
where: {
name: {[Op.iLike]: mockMember.name},
userid: authorId
}
})
})
test('if database.members.update returns 0 rows changed, throw error', async () => {
// Arrange
memberRepo.updateMemberField = jest.fn().mockResolvedValue(0);
database.members = {
update: jest.fn().mockResolvedValue([0])
};
// Act
await expect(memberHelper.updateMemberField(authorId, mockMember.name, "displayname", mockMember.displayname)).rejects.toThrow(`Can't update ${mockMember.name}. ${enums.err.NO_MEMBER}.`);
})
@@ -689,7 +702,7 @@ describe('MemberHelper', () => {
describe('checkIfProxyExists', () => {
beforeEach(() => {
memberRepo.getMembersByAuthor.mockResolvedValue([mockMember]);
jest.spyOn(memberHelper, "getMembersByAuthor").mockResolvedValue([mockMember]);
})
test.each([
@@ -707,8 +720,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.checkIfProxyExists(authorId, proxy)
// Assert
expect(res).toEqual(false)
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledTimes(1);
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledWith(authorId);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId);
})
test.each([
@@ -719,13 +732,13 @@ describe('MemberHelper', () => {
// Act & Assert
await expect(memberHelper.checkIfProxyExists(authorId, proxy)).rejects.toThrow(error);
expect(memberRepo.getMembersByAuthor).not.toHaveBeenCalled();
expect(memberHelper.getMembersByAuthor).not.toHaveBeenCalled();
})
test('--text returns correct error and calls getMemberByAuthor', async () => {
await expect(memberHelper.checkIfProxyExists(authorId, "--text")).rejects.toThrow(enums.err.PROXY_EXISTS);
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledTimes(1);
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledWith(authorId);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId);
})
})

View File

@@ -2,16 +2,16 @@ const env = require('dotenv');
env.config();
jest.mock('../../src/repositories/memberRepo.js', () => {
jest.mock('../../src/helpers/memberHelper.js', () => {
return {
memberRepo: {
memberHelper: {
getMembersByAuthor: jest.fn()
}
}
})
const {memberHelper} = require("../../src/helpers/memberHelper.js");
const {messageHelper} = require("../../src/helpers/messageHelper.js");
const {memberRepo} = require("../../src/repositories/memberRepo");
describe('messageHelper', () => {
@@ -54,7 +54,7 @@ describe('messageHelper', () => {
const attachmentUrl = "../oya.png"
beforeEach(() => {
memberRepo.getMembersByAuthor = jest.fn().mockImplementation((specificAuthorId) => {
memberHelper.getMembersByAuthor = jest.fn().mockImplementation((specificAuthorId) => {
if (specificAuthorId === "1") return membersFor1;
if (specificAuthorId === "2") return membersFor2;
if (specificAuthorId === "3") return membersFor3;

View File

@@ -1,16 +0,0 @@
{
"compilerOptions": {
"lib": [
"es2021"
],
"target": "es2021",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./database/build",
"rootDir": "./database",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true
}
}

View File

@@ -1,7 +0,0 @@
FLUXER_BOT_TOKEN=<>
POSTGRES_PASSWORD=<>
POSTGRES_ENDPOINT=postgres
PGADMIN_DEFAULT_EMAIL: <>
PGADMIN_DEFAULT_PASSWORD: <>
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'