4 Commits

Author SHA1 Message Date
8fe53563d0 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>
2026-03-01 21:25:49 -05:00
d14e89e8b2 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>
2026-02-28 15:28:27 -05:00
39a7115803 Converting ES6 back to CJS (#25)
converting back to cjs

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-02-28 14:39:32 -05:00
Laika Bozhko
72b70f5175 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>
2026-02-28 14:37:59 -05:00
18 changed files with 31 additions and 202 deletions

View File

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

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

@@ -1,26 +0,0 @@
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,4 +7,4 @@ FROM node:20-alpine
WORKDIR /app WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
COPY . . 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) [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 ## Commands
All commands are prefixed by `pf;`. Currently only a few are implemented. All commands are prefixed by `pf;`. Currently only a few are implemented.

View File

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

View File

@@ -7,13 +7,13 @@ env.config();
export const AppDataSource = new DataSource({ export const AppDataSource = new DataSource({
type: "postgres", type: "postgres",
host: process.env.POSTGRES_ENDPOINT, host: "localhost",
port: 5432, port: 5432,
username: "postgres", username: "postgres",
password: process.env.POSTGRES_PASSWORD, password: process.env.POSTGRES_PASSWORD,
database: "postgres", database: "postgres",
synchronize: false, synchronize: false,
logging: false, logging: true,
entities: [path.join(__dirname, "./entity/*.{ts,js}")], entities: [path.join(__dirname, "./entity/*.{ts,js}")],
migrations: [path.join(__dirname, "./migrations/*.{ts,js}")], migrations: [path.join(__dirname, "./migrations/*.{ts,js}")],
migrationsRun: true, migrationsRun: true,
@@ -23,4 +23,4 @@ export const AppDataSource = new DataSource({
null: "sql-null", null: "sql-null",
undefined: "throw", undefined: "throw",
}, },
}); });

View File

@@ -1,7 +1,6 @@
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Unique} from "typeorm" import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from "typeorm"
@Entity({name: "Member", synchronize: true}) @Entity({name: "Member", synchronize: true})
@Unique("UQ_Member_userid_name", ['userid', 'name'])
export class Member { export class Member {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()

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"`);
}
}

View File

@@ -35,7 +35,7 @@
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"start": "ts-node src/bot.js", "start": "ts-node src/bot.js",
"new-migration": "typeorm-ts-node-commonjs migration:create database/migrations/update", "build-db": "tsc",
"generate-db": "typeorm-ts-node-commonjs migration:generate -d database/data-source.ts 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" "run-migration": "typeorm-ts-node-commonjs migration:run -d database/data-source.ts"
} }

View File

@@ -10,7 +10,6 @@ const { AppDataSource } = require("../database/data-source");
env.config(); env.config();
const token = process.env.FLUXER_BOT_TOKEN; const token = process.env.FLUXER_BOT_TOKEN;
const debug = process.env.debug;
if (!token) { if (!token) {
console.error("Missing FLUXER_BOT_TOKEN environment variable."); console.error("Missing FLUXER_BOT_TOKEN environment variable.");
@@ -64,15 +63,12 @@ module.exports.handleMessageCreate = async function(message) {
} }
} }
catch(error) { catch(error) {
if(debug){console.error("An error occurred at unix timestamp " + Date.now() + "while processing the command: " + message + " with error:" + error);} console.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
} }
} }
client.on(Events.Ready, () => { client.on(Events.Ready, () => {
console.log(`Logged in as ${client.user?.username}`); console.log(`Logged in as ${client.user?.username}`);
if(debug){console.log(Date.now() + `: Currently running in debug mode!`)}
}); });
let guildCount = 0; let guildCount = 0;

View File

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

View File

@@ -1,4 +1,4 @@
const {memberRepo} = require('../repositories/memberRepo.js'); const {memberHelper} = require('./memberHelper.js');
const msgh = {}; const msgh = {};
@@ -39,7 +39,7 @@ msgh.parseCommandArgs = function(content, commandName) {
* @returns {Promise<{model, string, bool}>} The proxy message object. * @returns {Promise<{model, string, bool}>} The proxy message object.
*/ */
msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ 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 an author has no members, no sense in searching for proxy
if (members.length === 0) { if (members.length === 0) {
return; return;

View File

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

View File

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

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'