28 Commits

Author SHA1 Message Date
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
df80eca0ec refactor: Removing then/catch from async/await calls (#22)
* refactored async/await for import helper to not also use then/catch

* added enum

* refactor webhookHelper and tests to not use then/catch

* changed docstring

* refactoring bot and tests to not use then/catch

* refactoring commands.js and tests to not use then/catch

* refactoring memberHelper.js and tests to not use then/catch

* removing then/catch from messageHelper.test.js

* fixed set up for commands tests

* edited bot to have top level main function

* one more test in commands.js, and removed console.error

* fixed typo in webhookHelper

* forgot to switch over some tests in bot.test and commands.test

* removed console.log from import helper

* put console.error in commands

* converted utils.js to not use then/catch

* tested utils checkImageFormatValidity

* removed jest-fetch-mock since it turns out I was just manually mocking it anyway

* refactored database to not use then/catch

* added dash to commands.js and test to pass

* added the remaining webhook tests

* changed utils to check for 10MB size not 1MB

* removed unnecessary try/catch from utils

* Simplify getWebhook to use .find() instead of foreach logic

* make memberCommand exit when error occurs with parseMemberCommand

* changed commands.js to not have user interaction within the catch

* updated console.error message in database.js

* made importHelper mock throw error instead of "resolve" error

* replaced "pk;" with "pf;" in test

* Got rid of unnecessary check for empty message from user (Fluxer doesn't allow this to happen)

Removed export of token

* getAllMembersInfo checks for fields.length

* added default case to memberCommandHandler to throw error if command is not recognized

* reversed check for valid proxy (was returning valid if the proxy existed and invalid if it didn't)

* pushes e.message instead of full error object to errors array in importHelper

* adjusted tests to properly use mockRejectedValue for async rejections

* changed getAllMembersInfo map to say `index` not `name` as it actually gets the index of a member and then the member object

* adjusted importHelper to properly test throwing of aggregate error

* revamped setting of expiration warning (moved to utils and changed logic, wrote tests)

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-02-25 19:30:39 -05: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
10 changed files with 255 additions and 74 deletions

View File

@@ -0,0 +1,46 @@
name: Build Dev instance
on:
push:
branches: ["develop", "Develop"]
pull_request:
branches: ["develop", "Develop"]
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 up -d pluralflux-dev

View File

@@ -0,0 +1,46 @@
name: nodeJS remote worker
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
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 up -d pluralflux-prod

View File

@@ -0,0 +1,24 @@
name: Auto-Sync from Mirror
on:
push:
repository: "Pluralflux/Pluralflux"
branches: [main,develop]
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

@@ -1,21 +1,34 @@
services:
main:
build: .
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"
pluralflux-prod:
image: engineering.sanya.gay/pluralflux/pluralflux:latest
container_name: pluralflux-prod
restart: unless-stopped
networks:
- pluralflux-net
postgres:
env_file: "variables-prod.env"
postgres-dev:
image: postgres:latest
container_name: pluralflux-postgres
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pwd
secrets:
- postgres_pwd
container_name: pluralflux-dev-postgres
env_file: "variables.env"
volumes:
- pgdataDev:/var/lib/postgresql
- ./pgBackup:/mnt/pgBackup
networks:
- pluralflux-dev-net
postgres-prod:
image: postgres:latest
container_name: pluralflux-prod-postgres
env_file: "variables-prod.env"
volumes:
- pgdata:/var/lib/postgresql
ports:
- "5432:5432"
- ./pgBackup/prod/:/mnt/pgBackup/prod
networks:
- pluralflux-net
pgadmin:
@@ -23,24 +36,24 @@ services:
container_name: pluralflux-pgadmin
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: code@asterfialla.com
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/postgres_pwd
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
secrets:
- postgres_pwd
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:
secrets:
postgres_pwd:
file: ./secrets/postgres-password.txt
pgdataDev:
pgadmindata:
pgdata:

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

@@ -44,7 +44,10 @@ helperEnums.help = {
}
helperEnums.misc = {
ATTACHMENT_SENT_BY: "Attachment sent by:"
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/"
}
export const enums = helperEnums;

View File

@@ -283,8 +283,8 @@ mh.updatePropic = async function (authorId, memberName, values, attachmentUrl =
const imgUrl = values ?? attachmentUrl;
// Throws error if invalid
await utils.checkImageFormatValidity(imgUrl);
return await mh.updateMemberField(authorId, memberName, "propic", imgUrl, attachmentExpiration);
const expirationWarning = utils.setExpirationWarning(imgUrl, attachmentExpiration);
return await mh.updateMemberField(authorId, memberName, "propic", imgUrl, expirationWarning);
}
/**
@@ -369,15 +369,15 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro
if (propic && propic.length > 0) {
try {
isValidPropic = await utils.checkImageFormatValidity(propic);
}
catch(e) {
errors.push(`Tried to set profile picture to \"${propic}\". ${e.message}. ${enums.err.SET_TO_NULL}`);
isValidPropic = false;
}
}
if (isValidPropic && attachmentExpiration) {
errors.push(mh.setExpirationWarning(attachmentExpiration));
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
@@ -394,17 +394,11 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro
* @param {string} memberName - The member to update
* @param {string} columnName - The column name to update.
* @param {string} value - The value to update to.
* @param {string | null} [attachmentExpiration] - The attachment expiration date (if any)
* @param {string | null} [expirationWarning] - The attachment expiration warning (if any)
* @returns {Promise<string>} A successful update.
* @throws {Error} When no member row was updated.
*/
mh.updateMemberField = async function (authorId, memberName, columnName, value, attachmentExpiration = null) {
let fluxerPropicWarning;
// indicates that an attachment was uploaded on Fluxer directly
if (columnName === "propic" && attachmentExpiration) {
fluxerPropicWarning = mh.setExpirationWarning(value);
}
mh.updateMemberField = async function (authorId, memberName, columnName, value, expirationWarning = null) {
const res = await database.members.update({[columnName]: value}, {
where: {
name: {[Op.iLike]: memberName},
@@ -414,21 +408,7 @@ mh.updateMemberField = async function (authorId, memberName, columnName, value,
if (res[0] === 0) {
throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
} else {
return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`;
}
}
/**
* Sets the warning for an expiration date.
*
* @param {string} expirationString - An expiration date string.
* @returns {string} A description of the expiration, interpolating the expiration string.
*/
mh.setExpirationWarning = function (expirationString) {
let expirationDate = new Date(expirationString);
if (!isNaN(expirationDate.valueOf())) {
expirationDate = expirationDate.toDateString();
return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like <https://imgbb.com/> and link to it directly`
return `Updated ${columnName} for ${memberName} to ${value}${expirationWarning ? `. ${expirationWarning}.` : '.'}`;
}
}

View File

@@ -34,4 +34,24 @@ u.checkImageFormatValidity = async function (imageUrl) {
return true;
}
/**
* Sets the warning that a Fluxer-uploaded image will expire.
*
* @param {string | null} [imgUrl] - An image URL.
* @param {string | null} [expirationString] - An expiration date string.
* @returns {string | null} A description of the expiration, or null.
*/
u.setExpirationWarning = function (imgUrl = null, expirationString = null) {
if (imgUrl && imgUrl.startsWith(enums.misc.FLUXER_ATTACHMENT_URL)) {
return enums.misc.ATTACHMENT_EXPIRATION_WARNING;
}
else if (expirationString) {
let expirationDate = new Date(expirationString);
if (!isNaN(expirationDate.valueOf())) {
return `${enums.misc.ATTACHMENT_EXPIRATION_WARNING}. Expiration date: *${expirationString}*.`;
}
}
return null;
}
export const utils = u;

View File

@@ -39,7 +39,7 @@ describe('MemberHelper', () => {
name: "somePerson",
displayname: "Some Person",
proxy: "--text",
propic: attachmentUrl
propic: 'ono.png'
}
beforeEach(() => {
@@ -446,14 +446,13 @@ describe('MemberHelper', () => {
describe('updatePropic', () => {
test.each([
[null, attachmentUrl, null, attachmentUrl],
[mockMember.propic, null, null, mockMember.propic],
[mockMember.propic, attachmentUrl, null, attachmentUrl],
[null, attachmentUrl, attachmentExpiration, attachmentUrl]
[null, attachmentUrl, undefined, attachmentUrl],
[mockMember.propic, null, undefined, mockMember.propic],
[mockMember.propic, attachmentUrl, undefined, mockMember.propic],
])('calls checkImageFormatValidity and updateMemberField and returns string', async (imgUrl, attachmentUrl, attachmentExpiration, expected) => {
// Arrange
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
utils.setExpirationWarning = jest.fn().mockReturnValue(undefined);
// Act
const result = await memberHelper.updatePropic(authorId, mockMember.name, imgUrl, attachmentUrl, attachmentExpiration);
// Assert
@@ -461,7 +460,21 @@ describe('MemberHelper', () => {
expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1);
expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(expected);
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "propic", expected, attachmentExpiration);
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "propic", expected, undefined);
})
test('calls setExpirationWarning', async() => {
// Arrange
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
utils.setExpirationWarning = jest.fn().mockReturnValue(enums.misc.ATTACHMENT_EXPIRATION_WARNING);
// Act
const result = await memberHelper.updatePropic(authorId, mockMember.name, null, attachmentUrl, attachmentExpiration);
// Assert
expect(result).toEqual("Updated");
expect(utils.setExpirationWarning).toHaveBeenCalledTimes(1);
expect(utils.setExpirationWarning).toHaveBeenCalledWith(attachmentUrl, attachmentExpiration);
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "propic", attachmentUrl, enums.misc.ATTACHMENT_EXPIRATION_WARNING);
})
})
@@ -612,6 +625,17 @@ describe('MemberHelper', () => {
expect(database.members.create).toHaveBeenCalledTimes(1);
})
test('calls setExpirationWarning if attachmentExpiration exists', async () => {
// Arrange
utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true);
jest.spyOn(memberHelper, 'setExpirationWarning').mockReturnValue(`${enums.misc.ATTACHMENT_EXPIRATION_WARNING}`);
// Act
await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic, attachmentExpiration)
// Assert
expect(memberHelper.setExpirationWarning).toHaveBeenCalledTimes(1);
expect(memberHelper.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic, attachmentExpiration);
})
test('if all values are valid, call database.members.create', async () => {
// Arrange
jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(false);
@@ -644,22 +668,18 @@ describe('MemberHelper', () => {
};
})
test('calls setExpirationWarning if attachmentExpiration', async () => {
await memberHelper.updateMemberField(authorId, mockMember.name, "propic", mockMember.propic, attachmentExpiration)
expect(memberHelper.setExpirationWarning).toHaveBeenCalledTimes(1);
expect(memberHelper.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic);
})
test.each([
['name', mockMember.name, null, `Updated name for ${mockMember.name} to ${mockMember.name}`],
['displayname', mockMember.displayname, null, `Updated name for ${mockMember.name} to ${mockMember.displayname}`],
['proxy', mockMember.proxy, null, `Updated name for ${mockMember.name} to ${mockMember.proxy}`],
['propic', mockMember.propic, null, `Updated name for ${mockMember.name} to ${mockMember.propic}`],
['propic', mockMember.propic, attachmentExpiration, `Updated name for ${mockMember.name} to ${mockMember.propic} warning}`]
])('calls database.members.update with correct column and value and return string', async (columnName, value, attachmentExpiration) => {
['name', mockMember.name, undefined, `Updated name for ${mockMember.name} to ${mockMember.name}.`],
['displayname', mockMember.displayname, undefined, `Updated displayname for ${mockMember.name} to ${mockMember.displayname}.`],
['proxy', mockMember.proxy, undefined, `Updated proxy for ${mockMember.name} to ${mockMember.proxy}.`],
['propic', mockMember.propic, undefined, `Updated propic for ${mockMember.name} to ${mockMember.propic}.`],
['propic', mockMember.propic,
'warning', `Updated propic for ${mockMember.name} to ${mockMember.propic}. warning.`]
])('calls database.members.update with correct column and value and return string', async (columnName, value, attachmentExpiration, expected) => {
// Act
await memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration)
const res = await memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration)
// Assert
expect(res).toEqual(expected);
expect(database.members.update).toHaveBeenCalledTimes(1);
expect(database.members.update).toHaveBeenCalledWith({[columnName]: value}, {
where: {

View File

@@ -5,6 +5,7 @@ const {utils} = require("../../src/helpers/utils.js");
describe('utils', () => {
const attachmentUrl = 'oya.png';
const expirationString = new Date("2026-01-01").toDateString();
let blob;
beforeEach(() => {
@@ -60,6 +61,28 @@ describe('utils', () => {
})
})
describe('setExpirationWarning', () => {
test('sets warning if image Url starts with Fluxer host', () => {
// Act
const result = utils.setExpirationWarning(`${enums.misc.FLUXER_ATTACHMENT_URL}${attachmentUrl}`);
// Assert
expect(result).toEqual(enums.misc.ATTACHMENT_EXPIRATION_WARNING);
})
test('sets warning if expiration string exists', () => {
const result = utils.setExpirationWarning(null, expirationString);
// Assert
expect(result).toEqual(`${enums.misc.ATTACHMENT_EXPIRATION_WARNING}. Expiration date: *${expirationString}*.`);
})
test('returns null if img url does not start iwth fluxer host and no expiration', () => {
// Act
const result = utils.setExpirationWarning(attachmentUrl);
// Assert
expect(result).toBeNull();
})
})
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();