11 Commits

Author SHA1 Message Date
critters.zip
badc6baaf0 test: Increase test coverage (#39)
* test: Increase test coverage

* Assert console.error on failed client.login test

* Remove redundant removeMember test
2026-04-09 15:38:14 -04:00
Aster Fialla
96c2abc06a Merge branch 'main' into Develop
# Conflicts:
#	.gitea/workflows/build-main.yml
#	compose.yaml
2026-03-11 21:42:57 -04:00
c2a88804ad docs: Update readme and other tweaks (#37)
* edited build-main.yml to have workflow_dispatch

* re-added pgadmindata to docker compose

* removed disclaimer about being run on my personal laptop as that's no longer true

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-03-11 21:27:41 -04:00
2b31cc2ae9 perf: Merge develop into main (#36)
* Update dockerfile for standalone deployment (#23)

* Add files via upload

* Update Docker image for pluralflux service

* removing unnecessary network and container name definitions

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* Converting ES6 back to CJS (#25)

converting back to cjs

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* Fix: Further converting ES6 to CJS - Making exports named instead of default (#26)

* adding to git ignore

* making imports named not default to not break all my tests

* adjusted setup for memberhelper test

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* feat: add db migrations with typeORM (#28)

* adding typescript packages for typeORM

* add typeORM initial files

* updating package scripts

* updating compose.yaml to have an exposed port for the postgres

* modifying setup for typeORM

* update database stuff and and package.json to help generate migrations

* made models and migrations in typeORM

* delete unneeded database.js

* made database pattern ignored by jest

* remove sequelize

* separate member repo from member helper

* not sure why i made everything numbers in the model but it's fixed now

* edited package.json script

* remove unused index.ts

* adjusted files to reference repository correctly and appdatasource

* made appdatasource export as named

* removed start-db script

* added init to appdatasource in bot.js

* migrations finally!

* new migration matching model names I want

* updating tests

* removing testpathignore patterns since it seems to be unecessary?

* adjusting migrations to match current schema

* removed reference to secrets file

* delete old migration

* Revert "delete old migration"

This reverts commit db1efa39a7a80d8976878856250ccaac6a753ab2.

* Revert "adjusting migrations to match current schema"

This reverts commit ef89a83f6a2ef0643d6ace0a3fcf9c40f4bc6dd6.

* just deleted system creation since it's got nothing in it anyway

* renamed memberRepository to memberRepo for consistency

* added await back to parseMemberCommand call to memberArgumentHandler

* changed call to memberHelper.getMembersByAuthor to memberRepo

* renamed repo updateMemberValue to updateMemberField

* removed throw references in repo docstrings

* remove unneeded subscriber directory ref

* changed createdAt and updatedAt columns to be auto-generated

made member table have timezone

* changed casing of isInitialized in mock for bot.js

* removed % from ILike query so that it doesn't match substrings/wildcard

* renamed some stray updateMemberValue in mocks -> updateMemberField

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* feat: Add migration to migrate existing data to new member table (#29)

added migration to fill new Member table with data from Members

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* fix: update message helper reference hotfix (#30)

* forgot to update a reference in messageHelper to memberRepo instead of memberHelper

* turned off data-source logging

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* fix: update dockerfile to run npm start (#31)

fix: update dockerfile to run npm start (which runs ts-node) instead of node

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* added .env with examples, updated data-source to be access a docker container instead of relying on loopback

* i forgot to git add data-source.ts 🤦

* fix: changed property reference for createMember in repo (#32)

fix for createMember object references (was referencing non-existent properties)

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* fix: memberRepo methods syntax (#35)

* rearranged update member field and remove member to match expected structure in typeORM

* update docstring

* change insert to save in memberRepo

* added command in package.json

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* Delete duplicate members migration (#33)

* add migration to delete duplicates that currently exist in the db

* added a name attribute for consistency

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* Add unique index migration (#34)

* add migration to delete duplicates that currently exist in the db

* change model and migration to add a unique index constraint to id and name

* renamed unique index name to be readable

* redid model and migration to use @Unique instead of @Index

* remove //Here comment

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>

* why wont these workflows stay up gahdamn

---------

Co-authored-by: Laika Bozhko <63646916+LaikaBzko@users.noreply.github.com>
Co-authored-by: Aster Fialla <asterfialla@gmail.com>
Co-authored-by: laika <laika@sanya.gay>
2026-03-09 09:00:15 -04:00
b602e654ec why wont these workflows stay up gahdamn
All checks were successful
Build Dev instance / build (push) Successful in 1m40s
2026-03-06 17:15:51 -05:00
732ad36bba Add unique index migration (#34)
* add migration to delete duplicates that currently exist in the db

* change model and migration to add a unique index constraint to id and name

* renamed unique index name to be readable

* redid model and migration to use @Unique instead of @Index

* remove //Here comment

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-03-06 16:52:26 -05:00
8446559bfb Delete duplicate members migration (#33)
* add migration to delete duplicates that currently exist in the db

* added a name attribute for consistency

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-03-06 16:43:44 -05:00
14ef1581c1 fix: memberRepo methods syntax (#35)
* rearranged update member field and remove member to match expected structure in typeORM

* update docstring

* change insert to save in memberRepo

* added command in package.json

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-03-06 16:39:23 -05:00
10eab6de74 fix: changed property reference for createMember in repo (#32)
fix for createMember object references (was referencing non-existent properties)

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-03-05 13:16:29 -05:00
d8682c2a1b i forgot to git add data-source.ts 🤦 2026-03-05 17:25:39 +11:00
20e8564c15 added .env with examples, updated data-source to be access a docker container instead of relying on loopback 2026-03-05 17:22:22 +11:00
15 changed files with 241 additions and 23 deletions

View File

@@ -0,0 +1,49 @@
name: Build Dev instance
on:
push:
branches: ["develop", "Develop"]
pull_request:
branches: ["develop", "Develop"]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: login to gitea registry
uses: docker/login-action@v3
with:
registry: ${{ gitea.server_url }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
engineering.sanya.gay/pluralflux/pluralflux-dev:latest
- name: Deploy bot
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: 22
script: |
cd ${{ secrets.BOT_DIRECTORY }}
docker compose pull
docker compose up -d pluralflux-dev

View File

@@ -0,0 +1,49 @@
name: nodeJS remote worker
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: login to gitea registry
uses: docker/login-action@v3
with:
registry: ${{ gitea.server_url }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
engineering.sanya.gay/pluralflux/pluralflux:latest
- name: Deploy bot
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: 22
script: |
cd ${{ secrets.BOT_DIRECTORY }}
docker compose pull
docker compose up -d pluralflux-prod

View File

@@ -0,0 +1,26 @@
name: Auto-Sync from Mirror
on:
push:
repository: "Pluralflux/Pluralflux"
branches: [main,develop]
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout Fork
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN }}
- name: Pull from Mirror
run: |
git remote add upstream https://engineering.sanya.gay/PluralFlux/PluralFlux.git
git fetch upstream --prune
git reset --hard origin/main
git push origin "refs/remotes/upstream/*:refs/heads/*" --force-with-lease
git merge upstream/main -m "Syncing from github"
git push origin main

View File

@@ -7,8 +7,6 @@ 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

@@ -22,4 +22,5 @@ services:
volumes:
- pgadmindata:/var/lib/pgadmin
volumes:
pgdata:
pgdata:
pgadmindata:

View File

@@ -7,7 +7,7 @@ env.config();
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
host: process.env.POSTGRES_ENDPOINT,
port: 5432,
username: "postgres",
password: process.env.POSTGRES_PASSWORD,
@@ -23,4 +23,4 @@ export const AppDataSource = new DataSource({
null: "sql-null",
undefined: "throw",
},
});
});

View File

@@ -1,6 +1,7 @@
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from "typeorm"
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()

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,14 @@
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"`);
}
}

View File

@@ -35,7 +35,7 @@
"scripts": {
"test": "jest",
"start": "ts-node src/bot.js",
"build-db": "tsc",
"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"
}

View File

@@ -403,7 +403,7 @@ memberHelper.updateMemberField = async function (authorId, memberName, columnNam
/**
* Gets the details for a member.
*
* @param {{Members, string[]}} member - The member object
* @param {{Member, string[]}} member - The member object
* @returns {EmbedBuilder} The member's info.
*/
memberHelper.getMemberInfo = function (member) {

View File

@@ -36,12 +36,7 @@ memberRepo.getMembersByAuthor = async function (authorId) {
* @returns {Promise<number>} Number of results removed.
*/
memberRepo.removeMember = async function (authorId, memberName) {
const deleted = await members.delete({
where: {
name: ILike(memberName),
userid: authorId
}
})
const deleted = await members.delete({ name: ILike(memberName), userid: authorId })
return deleted.affected;
}
@@ -53,8 +48,8 @@ memberRepo.removeMember = async function (authorId, memberName) {
* @returns {Promise<Member>} A successful inserted object.
*/
memberRepo.createMember = async function (createObj) {
return members.insert({
name: createObj.name, userid: createObj.authorId, displayname: createObj.displayName, proxy: createObj.proxy, propic: createObj.propic
return await members.save({
name: createObj.name, userid: createObj.userid, displayname: createObj.displayname, proxy: createObj.proxy, propic: createObj.propic
});
}
@@ -69,12 +64,10 @@ memberRepo.createMember = async function (createObj) {
* @returns {Promise<number>} A successful update.
*/
memberRepo.updateMemberField = async function (authorId, memberName, columnName, value) {
const updated = await members.update({[columnName]: value}, {
where: {
name: ILike(memberName),
userid: authorId
}
})
const updated = await members.update({
name: ILike(memberName),
userid: authorId
}, {[columnName]: value})
return updated.affected;
}

View File

@@ -293,8 +293,23 @@ describe('bot', () => {
expect(client.login).toHaveBeenCalledWith(process.env.FLUXER_BOT_TOKEN)
})
test('login exits with code 1 if client.login fails', async () => {
// Arrange
let message = "client.login failed";
client.login = jest.fn().mockImplementation(() => { throw new Error(message) });
jest.spyOn(global.console, 'error').mockImplementation(() => {});
jest.spyOn(process, 'exit').mockImplementation(() => {});
// Act
await login();
// Assert
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith('Login failed:', new Error(message));
expect(process.exit).toHaveBeenCalledTimes(1);
expect(process.exit).toHaveBeenCalledWith(1);
})
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
})
})

View File

@@ -67,6 +67,7 @@ describe('MemberHelper', () => {
});
test.each([
[[mockMember.name], null, null, undefined, false, mockMember.name],
[[mockMember.name, '--help'], null, null, undefined, true, undefined],
[['new', '--help'], null, null, 'new', true, '--help'],
[['remove', '--help'], null, null, 'remove', true, '--help'],
@@ -474,6 +475,30 @@ describe('MemberHelper', () => {
})
})
describe('removeMember', () => {
test('if removeMember returns a positive number, return a success message', async () => {
// Arrange
memberRepo.removeMember.mockResolvedValue(1);
const expectedReturn = `Member "${mockMember.name}" has been deleted.`;
// Act
const res = await memberHelper.removeMember(authorId, mockMember.name);
// Assert
expect(memberRepo.removeMember).toHaveBeenCalledTimes(1);
expect(memberRepo.removeMember).toHaveBeenCalledWith(authorId, mockMember.name);
expect(res).toEqual(expectedReturn);
})
test('if removeMember returns a negative number, throw error', async () => {
// Arrange
memberRepo.removeMember.mockResolvedValue(-1);
// Act
expect(memberHelper.removeMember(authorId, mockMember.name)).rejects.toThrow(new Error(`${enums.err.NO_MEMBER}`));
// Assert
expect(memberRepo.removeMember).toHaveBeenCalledTimes(1);
expect(memberRepo.removeMember).toHaveBeenCalledWith(authorId, mockMember.name);
})
})
describe('addFullMember', () => {
test('calls getMemberByName', async () => {
// Arrange
@@ -504,6 +529,29 @@ describe('MemberHelper', () => {
expect(memberRepo.createMember).not.toHaveBeenCalled();
})
test('if displayName is not filled out, call memberRepo.createMember with null value', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue();
const expectedMemberArgs = {
name: mockMember.name,
userid: authorId,
displayname: null,
proxy: null,
propic: null
}
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {
member: expectedMemberArgs,
errors: [`Display name ${enums.err.NO_VALUE}. ${enums.err.SET_TO_NULL}`]
}
// Act
const res = await memberHelper.addFullMember(authorId, mockMember.name, " ");
// Assert
expect(res).toEqual(expectedReturn);
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
})
test('if displayname is over 32 characters, call memberRepo.createMember with null value', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue();

7
variables.env Normal file
View File

@@ -0,0 +1,7 @@
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'