160 Commits

Author SHA1 Message Date
Aster Fialla
db8f2c59de adjusted importHelper to properly test throwing of aggregate error 2026-02-24 22:29:03 -05:00
Aster Fialla
dd21ed564b changed getAllMembersInfo map to say index not name as it actually gets the index of a member and then the member object 2026-02-24 21:26:42 -05:00
Aster Fialla
d226238e14 adjusted tests to properly use mockRejectedValue for async rejections 2026-02-24 21:08:04 -05:00
Aster Fialla
5050f98e8f pushes e.message instead of full error object to errors array in importHelper 2026-02-24 20:38:39 -05:00
Aster Fialla
dad80bd385 reversed check for valid proxy (was returning valid if the proxy existed and invalid if it didn't) 2026-02-24 20:38:39 -05:00
Aster Fialla
545136bec7 added default case to memberCommandHandler to throw error if command is not recognized 2026-02-24 20:38:39 -05:00
Aster Fialla
b02937a660 getAllMembersInfo checks for fields.length 2026-02-24 20:38:39 -05:00
Aster Fialla
6d82a067a1 Got rid of unnecessary check for empty message from user (Fluxer doesn't allow this to happen)
Removed export of token
2026-02-24 20:38:37 -05:00
Aster Fialla
1d9d9600e5 replaced "pk;" with "pf;" in test 2026-02-24 20:08:52 -05:00
Aster Fialla
c7d5f8a341 made importHelper mock throw error instead of "resolve" error 2026-02-24 20:05:29 -05:00
Aster Fialla
15d2f7d79d updated console.error message in database.js 2026-02-24 20:02:32 -05:00
Aster Fialla
ba2274e5be changed commands.js to not have user interaction within the catch 2026-02-24 20:02:11 -05:00
Aster Fialla
de674281f9 make memberCommand exit when error occurs with parseMemberCommand 2026-02-24 19:54:10 -05:00
Aster Fialla
102d694240 Simplify getWebhook to use .find() instead of foreach logic 2026-02-24 19:52:29 -05:00
Aster Fialla
fb878222dd removed unnecessary try/catch from utils 2026-02-24 19:49:59 -05:00
Aster Fialla
a1e007448f changed utils to check for 10MB size not 1MB 2026-02-24 19:47:46 -05:00
Aster Fialla
efefcb69f4 added the remaining webhook tests 2026-02-24 18:58:45 -05:00
Aster Fialla
ad783ae6b7 added dash to commands.js and test to pass 2026-02-24 18:48:59 -05:00
Aster Fialla
664295e4a5 refactored database to not use then/catch 2026-02-24 18:12:43 -05:00
Aster Fialla
ded5910a7c removed jest-fetch-mock since it turns out I was just manually mocking it anyway 2026-02-24 18:06:13 -05:00
Aster Fialla
cdf5c6e537 tested utils checkImageFormatValidity 2026-02-24 18:04:15 -05:00
Aster Fialla
34cb8f3cdb converted utils.js to not use then/catch 2026-02-24 16:58:56 -05:00
Aster Fialla
8d946ce921 put console.error in commands 2026-02-24 16:58:39 -05:00
Aster Fialla
8919fce34f removed console.log from import helper 2026-02-24 16:56:03 -05:00
Aster Fialla
15a040d1d1 forgot to switch over some tests in bot.test and commands.test 2026-02-24 16:55:51 -05:00
Aster Fialla
6470b223f6 fixed typo in webhookHelper 2026-02-24 16:41:00 -05:00
Aster Fialla
5d8ff9c6c6 one more test in commands.js, and removed console.error 2026-02-24 16:22:38 -05:00
Aster Fialla
2bf218683a edited bot to have top level main function 2026-02-24 16:18:40 -05:00
Aster Fialla
8b8cdd6d12 fixed set up for commands tests 2026-02-24 15:57:33 -05:00
Aster Fialla
5a91a529b1 removing then/catch from messageHelper.test.js 2026-02-24 15:47:30 -05:00
Aster Fialla
b36a3ae236 refactoring memberHelper.js and tests to not use then/catch 2026-02-24 15:43:43 -05:00
Aster Fialla
c19614b6a1 refactoring commands.js and tests to not use then/catch 2026-02-24 14:50:46 -05:00
Aster Fialla
b4e4e5a951 refactoring bot and tests to not use then/catch 2026-02-24 14:13:48 -05:00
Aster Fialla
5ba5435d46 changed docstring 2026-02-24 13:56:46 -05:00
Aster Fialla
fc58c47b03 refactor webhookHelper and tests to not use then/catch 2026-02-24 13:56:37 -05:00
Aster Fialla
a0f62763af Merge branch 'main' of https://github.com/pieartsy/PluralFlux into refactor-async-await
# Conflicts:
#	src/helpers/importHelper.js
#	tests/helpers/importHelper.test.js
2026-02-24 12:43:52 -05:00
7fead5e3d7 feat: Add unit tests (#21)
Tests are mostly complete (though some are failing -- to be fixed in the
async/await refactor.

Also refactored, fixed bugs found while testing, and added ability to
type `pf;m` instead of `pf;member`.

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
2026-02-24 12:42:23 -05:00
Aster Fialla
8318d715ad added enum 2026-02-24 12:39:42 -05:00
Aster Fialla
971638bfe3 refactored async/await for import helper to not also use then/catch 2026-02-24 12:39:37 -05:00
Aster Fialla
123aabf650 documentation: adjusted docstrings 2026-02-24 10:37:02 -05:00
428310dfad Delete .github/workflows/node.js.yml (#20)
Since we're moving to local DevOps, this is no longer required. Also it
was broken.
2026-02-24 09:30:27 -05:00
dc0de4b092 Update README.md
added LLM note
2026-02-24 09:23:07 -05:00
6898e3142c Create CONTRIBUTING.md 2026-02-24 09:16:55 -05:00
Aster Fialla
3c7dfcb72b update enums 2026-02-23 15:55:01 -05:00
Aster Fialla
9059c63747 mostly finished command tests 2026-02-23 15:54:45 -05:00
Aster Fialla
6937d7b694 made import error not a PDF again 2026-02-23 15:51:12 -05:00
Aster Fialla
e31d9c2881 removed now-unneeded .toJSON() for embeds 2026-02-23 15:20:39 -05:00
Aster Fialla
654338f88e removed duplicate text from importHelper 2026-02-23 15:11:10 -05:00
Aster Fialla
2a2de70b46 starting commands tests 2026-02-23 15:10:57 -05:00
Aster Fialla
7dfc949b63 separated out commands to be more easily testable and added functionality for aliases 2026-02-23 15:10:48 -05:00
Aster Fialla
bb99ee7ef1 structure for utils test edited a bit 2026-02-23 13:38:38 -05:00
Aster Fialla
088eaaa654 package.lock 2026-02-23 13:36:58 -05:00
Aster Fialla
a2746a25f9 updated enums 2026-02-23 13:36:40 -05:00
Aster Fialla
daf23b2f5d adding import tests 2026-02-23 13:36:31 -05:00
Aster Fialla
813e893afb more accurate framework for utils tests 2026-02-23 12:30:40 -05:00
Aster Fialla
10c145a02d imported env files better 2026-02-23 12:27:25 -05:00
Aster Fialla
6638756a9a replaced reference to [text] in proxy command instructions so it's clearer in contrast to the instructions to type without brackets 2026-02-23 12:01:07 -05:00
Aster Fialla
df60ec489b updated package.json 2026-02-23 11:56:35 -05:00
Aster Fialla
79f0f19733 adjusted bot.test mock 2026-02-23 11:50:50 -05:00
Aster Fialla
91e98f2434 fixed bug with memberHelper not processing attached URL 2026-02-23 11:35:01 -05:00
Aster Fialla
c752ee2bf2 Revert "adding debounce" cus it's not working
This reverts commit 1d27db960c.

# Conflicts:
#	src/bot.js
2026-02-23 11:19:26 -05:00
Aster Fialla
bfb5bf9cbb testing bot login, added invoke for login 2026-02-23 10:47:02 -05:00
Aster Fialla
a2bd7b755c testing bot.js handleMessageCreate 2026-02-23 10:38:04 -05:00
Aster Fialla
0f65db2d42 add frameworks for other tests 2026-02-23 09:00:48 -05:00
Aster Fialla
da79db856a removed unneeded imports from messageHelper tests 2026-02-23 08:59:56 -05:00
Aster Fialla
50c41990ed removed unused argument from bot call to sendMessageAsMember 2026-02-23 08:59:32 -05:00
Aster Fialla
1d27db960c adding debounce 2026-02-23 08:47:27 -05:00
Aster Fialla
264c3e19fd removing some unused imports in messageHelper 2026-02-23 08:44:50 -05:00
Aster Fialla
0a1652f243 updated docstrings 2026-02-23 08:02:30 -05:00
Aster Fialla
580a0cb42e renaming test interpolation to be less silly 2026-02-23 07:53:03 -05:00
Aster Fialla
71c8736a38 added to docstring 2026-02-23 00:18:57 -05:00
Aster Fialla
d9530a2ccf structure and test revamp for memberHelper 2026-02-23 00:18:14 -05:00
Aster Fialla
72ec047f48 test frame for utils 2026-02-22 21:40:53 -05:00
Aster Fialla
be24034d48 updating tests and memberHelper to try and make parseMemberCommand less bloated/broken (not working ATM) 2026-02-22 21:40:42 -05:00
Aster Fialla
c3e7bf4433 updated enums 2026-02-22 18:48:52 -05:00
Aster Fialla
7dae9f52b1 fixed call to getMemberInfo to not include authorId 2026-02-22 18:32:47 -05:00
Aster Fialla
630ce7888c adjusted parsing logic to not try and call member when none exists (due to "new" command) 2026-02-22 18:06:18 -05:00
Aster Fialla
b88b9252be forgot to import enums into utils 2026-02-22 17:09:02 -05:00
Aster Fialla
79cfc1563d add license 2026-02-22 12:03:44 -05:00
adcb05a38b Create LICENSE
Add the peer production license as license
2026-02-22 11:57:12 -05:00
Aster Fialla
fa4ca779ee accidental error with memberHelper not allowing pf;member fixed 2026-02-22 11:52:51 -05:00
Aster Fialla
eb7340faf7 finished relevant tests for memberHelper 2026-02-22 11:24:57 -05:00
Aster Fialla
d4adbc028a typo not importing utils right 2026-02-22 09:54:01 -05:00
Aster Fialla
57336b4e58 added to enums 2026-02-21 23:05:59 -05:00
Aster Fialla
71803fd10c made utils file 2026-02-21 23:05:15 -05:00
Aster Fialla
cc8cb6e925 write more tests for addNewMember 2026-02-21 23:05:01 -05:00
Aster Fialla
a797e9e06d write more tests for parseMemberCommand 2026-02-21 21:37:57 -05:00
Aster Fialla
4146591c90 figured out issue (was missing return value) 2026-02-21 20:09:39 -05:00
Aster Fialla
db3c588745 updated tests 2026-02-20 11:36:41 -05:00
Aster Fialla
56d7f7d1fa call getMemberCommandInfo in parseMemberCommand 2026-02-20 11:33:16 -05:00
Aster Fialla
8f6ae668b0 update getMemberCommandInfo
# Conflicts:
#	src/helpers/memberHelper.js
2026-02-20 11:31:54 -05:00
Aster Fialla
211a705f31 update enums 2026-02-20 11:29:28 -05:00
Aster Fialla
0bca359b7f trying to get refactored memberhelper to pass tests -_- 2026-02-20 11:28:28 -05:00
Aster Fialla
932fc9c967 update getMemberCommandInfo 2026-02-20 11:03:50 -05:00
Aster Fialla
cd01473121 update enums 2026-02-20 11:00:57 -05:00
Aster Fialla
c133ee2c42 refactored memberHelper 2026-02-20 11:00:21 -05:00
Aster Fialla
f055518b1f Merge branch 'main' into add-tests
# Conflicts:
#	README.md
2026-02-20 10:02:13 -05:00
Aster Fialla
69c242350f updated readme 2026-02-20 10:01:56 -05:00
Aster Fialla
3ed24458e0 Merge branch 'main' of https://github.com/pieartsy/PluralFlux into add-tests 2026-02-19 21:44:07 -05:00
Aster Fialla
a30d408c02 Merge branch 'main' of https://github.com/pieartsy/PluralFlux into add-tests 2026-02-19 21:41:59 -05:00
Aster Fialla
e9052624d4 Merge branch 'main' of https://github.com/pieartsy/PluralFlux into add-tests 2026-02-19 21:41:29 -05:00
Aster Fialla
4d3db8c001 updated documentation 2026-02-19 21:38:10 -05:00
Aster Fialla
eb6c60618b added nicer error listing to importHelper 2026-02-19 21:38:03 -05:00
Aster Fialla
b64ba6e787 made it easier to make a member 2026-02-19 21:36:19 -05:00
Aster Fialla
d640322392 removed unnecessary await and console.log 2026-02-19 20:32:35 -05:00
Aster Fialla
b1a29fd3ff edit enums, tweak import content command 2026-02-19 20:30:55 -05:00
Aster Fialla
e7198230a7 updated member helper and tests 2026-02-19 20:08:26 -05:00
Aster Fialla
fad6d42ee2 updated to be enum 2026-02-19 19:32:37 -05:00
Aster Fialla
c4c6ad0fdd fixed bug for import not sending help text, added help text if you type a unrecognized command 2026-02-19 19:31:56 -05:00
Aster Fialla
1e2724bbfb update bot to suppress errors from API 2026-02-19 19:13:54 -05:00
Aster Fialla
849acf7275 update message helper test to include space case 2026-02-19 18:54:35 -05:00
Aster Fialla
7a3b8c1994 edited help message trigger for updatePropic 2026-02-19 01:45:43 -05:00
Aster Fialla
2e0a8adec5 added tests for updateDisplayName 2026-02-19 01:31:38 -05:00
Aster Fialla
7aeae1837f added todo note 2026-02-19 00:59:57 -05:00
Aster Fialla
6eb9fef376 added debounce to count guilds properly 2026-02-19 00:52:06 -05:00
Aster Fialla
9dab429d0d , updated tests for webhookHelper and removed error response when proxy is sent without content 2026-02-19 00:51:33 -05:00
Aster Fialla
f9199f8477 removed error response when proxy is sent without content 2026-02-19 00:48:57 -05:00
Aster Fialla
a7cd4e96f0 updating enums 2026-02-19 00:20:53 -05:00
Aster Fialla
21efbccfd7 adding more cases to messageHelper tests 2026-02-19 00:20:46 -05:00
Aster Fialla
873959a5f4 finally mocking correctly 2026-02-18 21:24:41 -05:00
Aster Fialla
d33c3213f3 removed confusing brackets from enum docs 2026-02-18 21:20:56 -05:00
Aster Fialla
75c4c548d8 deleted extra file added during merge 2026-02-18 21:13:25 -05:00
Aster Fialla
9d5493e8ab Merge remote-tracking branch 'origin/add-tests' into add-tests
# Conflicts:
#	src/bot.js
#	src/commands.js
#	src/database.js
#	src/helpers/importHelper.js
#	src/helpers/memberHelper.js
#	src/helpers/messageHelper.js
#	src/helpers/webhookHelper.js
2026-02-18 21:03:44 -05:00
Aster Fialla
fc1c463696 more tests for webhookhelper 2026-02-18 17:14:04 -05:00
Aster Fialla
1bba8099e9 updating docstring for messageHelper parseProxyTags 2026-02-18 16:29:08 -05:00
Aster Fialla
acd9ce7c3e more cases for messageHelper just in case 2026-02-18 16:28:35 -05:00
Aster Fialla
da9a3d2c8a finished tests for messageHelper! 2026-02-18 12:37:39 -05:00
Aster Fialla
274f1ead15 added "return" so tests dont terminate on failure and deleted env.jest 2026-02-18 12:11:55 -05:00
Aster Fialla
223292c2d3 added tests for parseProxyTags and updated logic 2026-02-18 10:17:08 -05:00
Aster Fialla
400e40a405 updated sendMessageAsAttachment to returnBufferFromText and updated commands/webhookHelper accordingly 2026-02-18 09:19:44 -05:00
Aster Fialla
152bc8873d added test for memberHelper 2026-02-18 09:16:26 -05:00
Aster Fialla
e16694ac2d fixed test and logic 2026-02-18 08:47:37 -05:00
Aster Fialla
f0ac02e86d readded line i shouldn't have removed in sendMessageAsMember 2026-02-18 00:33:05 -05:00
Aster Fialla
5c01f2e284 test setup for messagehelper and webhookhelper 2026-02-18 00:28:18 -05:00
Aster Fialla
da5a250445 added return to addFullMember so that addNewMember can reference it properly in strings 2026-02-18 00:27:51 -05:00
Aster Fialla
23a57b3e99 simplified sendMessageAsAttachment 2026-02-18 00:26:51 -05:00
Aster Fialla
1bf6c8c1f2 think i fixed weird error with webhook sending error when a user has no members 2026-02-18 00:25:41 -05:00
Aster Fialla
fe00f66104 more tests for member helper 2026-02-17 23:03:45 -05:00
Aster Fialla
15703c24cd moved import to helpers folder 2026-02-17 23:03:19 -05:00
Aster Fialla
3dbbe7df50 moved import to helpers folder 2026-02-17 23:03:08 -05:00
Aster Fialla
31eb4262dd upgraded fluxer.js 2026-02-17 22:23:55 -05:00
Aster Fialla
c645bb0aea renamed db to database
more tests and fixing logic for memberhelper
2026-02-17 22:23:47 -05:00
Aster Fialla
0b7f549bdf added error handling parseMemberCommand test 2026-02-17 21:21:07 -05:00
Aster Fialla
bfc633a755 setup fixed more 2026-02-17 20:52:22 -05:00
Aster Fialla
01e620a935 finally figured out issue with tests (referencing the method directly in the test.each calls the real method not the mock in beforeEach()) 2026-02-17 20:49:19 -05:00
Aster Fialla
a4804c2ea7 added babel to convert es modules to cjs 2026-02-17 20:48:44 -05:00
Aster Fialla
164ff7d8b6 nevermind it wasn't actually working, gonna move on for now 2026-02-17 19:58:56 -05:00
Aster Fialla
5e3b3f33d3 mostly working except for the weirdest error 2026-02-17 19:38:50 -05:00
Aster Fialla
4fcb53482c separating out enum return from method return 2026-02-17 19:36:14 -05:00
Aster Fialla
ba9552b4aa updated jest to sort of work with es6 2026-02-17 17:38:06 -05:00
Aster Fialla
35b454bc80 Revert "converted import syntax to commonJS"
This reverts commit 5ab0d62b
2026-02-17 17:25:18 -05:00
Aster Fialla
321fe7f0a9 more correct dockerfile and compose.yaml 2026-02-17 17:16:48 -05:00
Aster Fialla
0a4bfa59ad more correct test setup 2026-02-17 17:16:10 -05:00
Aster Fialla
79d98c3618 adjusted beforeeach/beforeall so more pass 2026-02-17 17:16:10 -05:00
Aster Fialla
a44e2745c5 got test sort of working (jest set up is not crashing but also not mocking correctly) 2026-02-17 17:16:10 -05:00
Aster Fialla
5ab0d62bdb converted import syntax to commonJS
removed unused methods
2026-02-17 17:16:09 -05:00
Aster Fialla
876f9486ad more correct test setup 2026-02-17 07:14:47 -05:00
Aster Fialla
5e28cdfd01 adjusted beforeeach/beforeall so more pass 2026-02-17 07:11:22 -05:00
Aster Fialla
5a39610547 got test sort of working (jest set up is not crashing but also not mocking correctly) 2026-02-16 18:46:33 -05:00
Aster Fialla
a3caa2dc42 converted import syntax to ES modules
removed unused methods
2026-02-16 18:46:32 -05:00
24 changed files with 2184 additions and 1234 deletions

View File

@@ -1,5 +1,5 @@
**/.dockerignore
**/.env
.env.jest
**/.git
**/.gitignore
**/.project

2
.env.jest Normal file
View File

@@ -0,0 +1,2 @@
FLUXER_BOT_TOKEN=jest-fluxer-bot-token
POSTGRES_PASSWORD=jest-postgres-password

View File

@@ -1,39 +0,0 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [25.3.0]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Run tests
run: npm test
working-directory: tests
- name: Tests failed
if: failure()
run: exit 1
- name: Tests passed
run: npm run build --if-present
working-directory: src

32
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,32 @@
Thanks for being interested in contributing to PluralFlux! I really can't do this by myself, nor do I want to!
This is a guide for code contributions only. If you're looking to contribute _money_, please go to my [sponsorship page](https://github.com/sponsors/pieartsy)!
## Disclaimer
The PluralFlux team is endogenic-friendly. Even if you disagree with this, keep discourse takes to yourself. If you can't be civil about it, please do not contribute. Other bigotry (transphobia, racism, ableism, fatphobia, etc) will not be tolerated either.
## Resources:
Not too many right now, but I'm hoping to get a wiki up.
- [Issues tracker](https://github.com/pieartsy/PluralFlux/issues)
- [Pluralflux Support server](https://fluxer.gg/WaO6qGdU) where you can contact me (there's a #contributing channel for contributors)
- You can also reach me @pieartsy on Discord (or anywhere, really) if/when Fluxer is down.
## Requirements
- [Fluxer.js](https://fluxerjs.blstmo.com/)
- Docker
- Node version 25.3.8
## Submitting changes
- Submit a pull request to this repository and explain your code and changes.
- We squash-merge commits, but keep to the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) structure for your PR titles. Conventions are not necessary for commits themselves, but try to keep them readable anyway.
- Branches should target one specific issue in the Issue Tracker and try not to touch other features. Link to the issue in your PR.
- All commits will undergo PR review, at minimum by the main dev right now. If you can't explain or defend your code, it may be rejected.
## Standards
- Docstrings are *mandatory*, following the standards in [JSDoc](https://michaelcurrin.github.io/dev-cheatsheets/cheatsheets/javascript/general/jsdoc.html).
- Comments are encouraged for confusing code. Prioritize readability (for example, just write an if/else instead of chaining ternaries).
- Reusable message replies should go in the enums file so we don't have to hunt them down to change wording.
- We use [jest](https://jestjs.io/) for testing. Please write unit tests and ideally integration tests for your code. Shoot for 60% coverage at minimum. Check that other features that touch your changes don't break.
### LLM usage
**Do *not* insert code that has been LLM/GenAI generated.** All code you submit must be handwritten by yourself. This includes writing tests. Vibe coding is especially **not** allowed. Please disclose if you've used any AI for any other reasons, such as rubber-ducking or figuring out bugs or something. The main dev is somewhat more open to these uses because of search engines enshittifying--but frequent LLM usage is heavily discouraged due to the ethical concerns as well as damage to critical thinking skills. Only turn to LLMs if scouring search engines, Stack Overflow, and your friends list has not worked.

60
LICENSE Normal file
View File

@@ -0,0 +1,60 @@
Peer Production License
Created by John Magyar, B.A., J.D. and Dmytri Kleiner, the following Peer Production License, a model for a Copyfarleft license, has been derived from the Creative Commons 'Attribution-NonCommercial-ShareAlike' license available at http://creativecommons.org/licenses/by-nc-sa/3.0/legalcode.
LICENSE
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS COPYFARLEFT PUBLIC LICENSE ("LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS LICENSE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN AS CONSIDERATION FOR ACCEPTING THE TERMS AND CONDITIONS OF THIS LICENSE AND FOR AGREEING TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS LICENSE.
1. DEFINITIONS
a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License.
b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License.
c. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale, gift or any other transfer of possession or ownership.
d. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast.
f. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work.
g. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
h. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images.
i. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium.
2. FAIR DEALING RIGHTS
Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws.
3. LICENSE GRANT
Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections;
b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and,
d. to Distribute and Publicly Perform Adaptations. The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved, including but not limited to the rights set forth in Section 4(f).
4. RESTRICTIONS
The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(d), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(d), as requested.
b. Subject to the exception in Section 4(c), you may not exercise any of the rights granted to You in Section 3 above in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation. The exchange of the Work for other copyrighted works by means of digital file-sharing or otherwise shall not be considered to be intended for or directed toward commercial advantage or private monetary compensation, provided there is no payment of any monetary compensation in connection with the exchange of copyrighted works.
c. You may exercise the rights granted in Section 3 for commercial purposes only if:
i. You are a worker-owned business or worker-owned collective; and
ii. all financial gain, surplus, profits and benefits produced by the business or collective are distributed among the worker-owners
d. Any use by a business that is privately owned and managed, and that seeks to generate profit from the labor of employees paid by salary or other wages, is not permitted under this license.
e. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and, (iv) consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(d) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
f. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License;
ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License if Your exercise of such rights is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b) and otherwise waives the right to collect royalties through any statutory or compulsory licensing scheme; and,
iii. Voluntary License Schemes. The Licensor reserves the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License that is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b).
g. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise.
5. REPRESENTATIONS, WARRANTIES AND DISCLAIMER
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. LIMITATION ON LIABILITY
EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. TERMINATION
a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
8. MISCELLANEOUS
a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law.

View File

@@ -36,13 +36,10 @@ All commands are prefixed by `pf;`. Currently only a few are implemented.
1. Pass in a direct remote image URL, for example: `pf;member jane propic <https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg>`. You can upload images on sites like <https://imgbb.com/>.
2. Upload an attachment directly.
**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.
- `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**
## Notes
- Attaching files to messages with the proxy does not work, due to either the limitations of Fluxer itself :(
- `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy A{text}` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**
## Upcoming
- [ ] React with x to delete message
- [ ] System tag at the end of messages
- [ ] Optionally keep proxy tag in message
- [ ] Autoproxy front
Check for, and add, feature requests in the [Issues tracker](https://github.com/pieartsy/PluralFlux/issues).
## LLM note
I do **not** use LLMs or other GenAI to generate code, nor do I ever plan to. _Very_ rarely, I ask questions of LLMs to troubleshoot bugs after search engines/StackOverflow/friends' knowledge has failed me, but that should lessen even more over time. As well, I used the Docker "Gordon" LLM to fix the many errors in my initial docker compose, but now that I have a devops person helping me, that should never happen again.

249
package-lock.json generated
View File

@@ -8,9 +8,8 @@
"name": "pluralflux",
"version": "1.0.0",
"dependencies": {
"@fluxerjs/core": "^1.1.5",
"@fluxerjs/core": "^1.2.2",
"dotenv": "^17.3.1",
"node-fetch": "^3.3.2",
"pg": "^8.18.0",
"pg-hstore": "^2.3.4",
"pm2": "^6.0.14",
@@ -21,7 +20,9 @@
"@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",
"babel-jest": "^30.2.0",
"fetch-mock": "^12.6.0",
"jest": "^30.2.0"
}
},
@@ -1854,97 +1855,90 @@
"tslib": "^2.4.0"
}
},
"node_modules/@fluxerjs/builders": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/builders/-/builders-1.1.5.tgz",
"integrity": "sha512-E7O+uf/DwgrCNkbxgo9/0FzhmP3a2ZH6SBJX9R+yUSsa07u5CdelrIatlp27UyoeBgO5HhXGMdncxa1geyf1tg==",
"license": "Apache-2.0",
"node_modules/@fetch-mock/jest": {
"version": "0.2.20",
"resolved": "https://registry.npmjs.org/@fetch-mock/jest/-/jest-0.2.20.tgz",
"integrity": "sha512-DGX2bhBInodaWPMV3+UZ530aVM3wDj16sAPjFzkrwb0JwNWIQK07CNbYprQ3Tmd2ixDJeaNx2E0aNb+hRb8FFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@fluxerjs/types": "1.1.5",
"@fluxerjs/util": "1.1.5"
"fetch-mock": "^12.6.0"
},
"engines": {
"node": ">=18.11.0"
},
"peerDependencies": {
"@jest/globals": "*",
"jest": "*"
}
},
"node_modules/@fluxerjs/builders/node_modules/@fluxerjs/types": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz",
"integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==",
"node_modules/@fluxerjs/builders": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/builders/-/builders-1.2.2.tgz",
"integrity": "sha512-647yuM44Fs+Cf/MKo01XRWoVb7RoFdSOBHebFiTLLCTO/tqz+dgZ4wgVsDNu9O/BB/oZ2tjAp9jDTxvXMXkUbA==",
"license": "Apache-2.0",
"dependencies": {
"@fluxerjs/types": "1.2.2",
"@fluxerjs/util": "1.2.2"
}
},
"node_modules/@fluxerjs/collection": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/collection/-/collection-1.2.2.tgz",
"integrity": "sha512-ryytiFh38gELqOLHBtL6Tz4f7/7DIu9L2CMTOqNYoer7E1ef8ukU7YiyANASWHgTf34hcGMC0tdi0dcFFFKkKQ==",
"license": "Apache-2.0"
},
"node_modules/@fluxerjs/core": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/core/-/core-1.1.5.tgz",
"integrity": "sha512-fklZePpz5SUZ0XQ9xU4CtKjtxUSnBrwRINKOXrEK6F8GWZgYotAKxc3Qkd07HRLP37EvOsuEl03GBnD+mq+LAQ==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/core/-/core-1.2.2.tgz",
"integrity": "sha512-P+c1jveYqwWGT6vafw+U1WmDPJ0Brp+aNa52Qp2vSsEQ3FLNUg+/25PhyrcPP0x2Mf7zoa1Wv9zGLlkHlKb9Ag==",
"license": "Apache-2.0",
"dependencies": {
"@fluxerjs/builders": "1.1.5",
"@fluxerjs/collection": "1.1.5",
"@fluxerjs/rest": "1.1.5",
"@fluxerjs/types": "1.1.5",
"@fluxerjs/util": "1.1.5",
"@fluxerjs/ws": "1.1.5"
"@fluxerjs/builders": "1.2.2",
"@fluxerjs/collection": "1.2.2",
"@fluxerjs/rest": "1.2.2",
"@fluxerjs/types": "1.2.2",
"@fluxerjs/util": "1.2.2",
"@fluxerjs/ws": "1.2.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@fluxerjs/core/node_modules/@fluxerjs/collection": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/collection/-/collection-1.1.5.tgz",
"integrity": "sha512-ms72WuFzLtnl8wYM5kAJh4vQovE8K8m0yyqjKxPFHUaTyXIJXAKPR+HmUOvWGj/vYKOdy7Pnz/+QuVho4ayCHQ==",
"license": "Apache-2.0"
},
"node_modules/@fluxerjs/core/node_modules/@fluxerjs/types": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz",
"integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==",
"license": "Apache-2.0"
},
"node_modules/@fluxerjs/rest": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/rest/-/rest-1.1.5.tgz",
"integrity": "sha512-LwksYkjSEEYYb8B/L7xPbrHjFrX8naivOY8aQ/ZjkZrO5pdmGYM4QPAcF8sgTZxZJaQdDHoxXr5ivDr459pApg==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/rest/-/rest-1.2.2.tgz",
"integrity": "sha512-Y96A2wQJ8akiAPpiyQm2WWBfNvrQBGIxIuwFCTuXkWg/g+0D6hYrVB7VHxQlnBeDUlj4T+fSJiqSoAYQ3pwrCQ==",
"license": "Apache-2.0",
"dependencies": {
"@fluxerjs/types": "1.1.5"
"@fluxerjs/types": "1.2.2"
}
},
"node_modules/@fluxerjs/rest/node_modules/@fluxerjs/types": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz",
"integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==",
"node_modules/@fluxerjs/types": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.2.2.tgz",
"integrity": "sha512-z3AcTxVF2iY/D0XR8xGIcx+c6LY6eNLWR0uO46xNGmEqpm5guE3joDz/EN8DfAZXuap/ludgqX6EA8dLADIeMg==",
"license": "Apache-2.0"
},
"node_modules/@fluxerjs/util": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/util/-/util-1.1.5.tgz",
"integrity": "sha512-W3XfAXZ3wHQ7++MxK7sL/s8vfT2x9GFAu6e7rSkDOnAvLx4Uq5E7yXEuLRnvYEOejNd9xP960Ut/RXX6Zlvl9g==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/util/-/util-1.2.2.tgz",
"integrity": "sha512-H0c6rKufJJQsz7cloZSK1EqqbRCaxEIF2LdJ4cIRiGPczQTleD2xAIOQ2NvBZxnFvwx+OeetZs79b42bsjcjHQ==",
"license": "Apache-2.0",
"dependencies": {
"@fluxerjs/types": "1.1.5"
"@fluxerjs/types": "1.2.2"
}
},
"node_modules/@fluxerjs/util/node_modules/@fluxerjs/types": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz",
"integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==",
"license": "Apache-2.0"
},
"node_modules/@fluxerjs/ws": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/ws/-/ws-1.1.5.tgz",
"integrity": "sha512-jzMugI6N/ZopI3MPwgaDQ09qUe4fm0L2LR5YAxekuVMImkx0eKSDpTtl/rEGXvLQjvDbMDrzvNJ17ku8we/Udg==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@fluxerjs/ws/-/ws-1.2.2.tgz",
"integrity": "sha512-twg1wYRYo4DlEHsVHSuUhSAKGK2qTmHM2PV3LNSdE0HdGQIB1EFviSuo0rWDpCb1sVTwIzBVMtthrlxyPfVtKA==",
"license": "Apache-2.0",
"dependencies": {
"@fluxerjs/types": "1.1.5",
"@fluxerjs/types": "1.2.2",
"ws": "^8.18.0"
}
},
"node_modules/@fluxerjs/ws/node_modules/@fluxerjs/types": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz",
"integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==",
"license": "Apache-2.0"
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2143,6 +2137,7 @@
"integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/environment": "30.2.0",
"@jest/expect": "30.2.0",
@@ -2813,6 +2808,13 @@
"@types/ms": "*"
}
},
"node_modules/@types/glob-to-regexp": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz",
"integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -3947,6 +3949,16 @@
"node": ">= 14"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -4206,27 +4218,20 @@
"integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==",
"license": "MIT"
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"node_modules/fetch-mock": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz",
"integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
"@types/glob-to-regexp": "^0.4.4",
"dequal": "^2.0.3",
"glob-to-regexp": "^0.4.1",
"regexparam": "^3.0.0"
},
"engines": {
"node": "^12.20 || >= 14.13"
"node": ">=18.11.0"
}
},
"node_modules/fill-range": {
@@ -4292,18 +4297,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4437,6 +4430,13 @@
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4781,6 +4781,7 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -5699,53 +5700,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-fetch/node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -6504,6 +6458,16 @@
"node": ">=4"
}
},
"node_modules/regexparam": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz",
"integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/regexpu-core": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
@@ -7470,15 +7434,6 @@
"makeerror": "1.0.12"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -9,7 +9,7 @@
},
"private": true,
"dependencies": {
"@fluxerjs/core": "^1.1.5",
"@fluxerjs/core": "^1.2.2",
"dotenv": "^17.3.1",
"pg": "^8.18.0",
"pg-hstore": "^2.3.4",
@@ -21,7 +21,9 @@
"@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",
"babel-jest": "^30.2.0",
"fetch-mock": "^12.6.0",
"jest": "^30.2.0"
},
"scripts": {

View File

@@ -1,11 +1,12 @@
import { Client, Events } from '@fluxerjs/core';
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 * as env from 'dotenv';
import env from 'dotenv';
import {utils} from "./helpers/utils.js";
env.config();
env.config({path: './.env'});
const token = process.env.FLUXER_BOT_TOKEN;
@@ -14,35 +15,45 @@ if (!token) {
process.exit(1);
}
const client = new Client({ intents: 0 });
export const client = new Client({ intents: 0 });
client.on(Events.MessageCreate, async (message) => {
try {
// Ignore bots and messages without content
if (message.author.bot || !message.content) return;
await handleMessageCreate(message);
});
/**
* Calls functions based off the contents of a message object.
*
* @async
* @param {Message} message - The message object
*
**/
export const handleMessageCreate = async function(message) {
try {
// Ignore bots
if (message.author.bot) return;
// Parse command and arguments
const content = message.content.trim();
// If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return.
if (!content.startsWith(messageHelper.prefix)) {
await webhookHelper.sendMessageAsMember(client, message, content).catch((e) => {
throw e
});
return;
return await webhookHelper.sendMessageAsMember(client, message);
}
const commandName = content.slice(messageHelper.prefix.length).split(" ")[0];
// If there's no command name (ie just the prefix)
if (!commandName) return await message.reply(enums.help.SHORT_DESC_PLURALFLUX);
const args = messageHelper.parseCommandArgs(content, commandName);
const command = commands.get(commandName);
let command = commands.commandsMap.get(commandName)
if (!command) {
const commandFromAlias = commands.aliasesMap.get(commandName);
command = commandFromAlias ? commands.commandsMap.get(commandFromAlias.command) : null;
}
if (command) {
await command.execute(message, client, args).catch(e => {
throw e
});
await command.execute(message, args);
}
else {
await message.reply(enums.err.COMMAND_NOT_RECOGNIZED);
@@ -50,9 +61,8 @@ client.on(Events.MessageCreate, async (message) => {
}
catch(error) {
console.error(error);
// return await message.reply(error.message);
}
});
}
client.on(Events.Ready, () => {
console.log(`Logged in as ${client.user?.username}`);
@@ -61,27 +71,29 @@ client.on(Events.Ready, () => {
let guildCount = 0;
client.on(Events.GuildCreate, () => {
guildCount++;
callback();
debouncePrintGuilds();
});
function printGuilds() {
console.log(`Serving ${client.guilds.size} guild(s)`);
}
const callback = Debounce(printGuilds, 2000);
const debouncePrintGuilds = utils.debounce(printGuilds, 2000);
export const debounceLogin = utils.debounce(client.login, 60000);
function Debounce(func, delay) {
let timeout = null;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
export const login = async function() {
try {
await client.login(token);
// await db.check_connection();
} catch (err) {
console.error('Login failed:', err);
process.exit(1);
}
}
try {
await client.login(token);
// await db.check_connection();
} catch (err) {
console.error('Login failed:', err);
process.exit(1);
}
function main()
{
login();
}
main();

View File

@@ -4,33 +4,59 @@ import {memberHelper} from "./helpers/memberHelper.js";
import {EmbedBuilder} from "@fluxerjs/core";
import {importHelper} from "./helpers/importHelper.js";
const cmds = new Map();
const cmds = {
commandsMap: new Map(),
aliasesMap: new Map()
};
cmds.set('member', {
cmds.aliasesMap.set('m', {command: 'member'})
cmds.commandsMap.set('member', {
description: enums.help.SHORT_DESC_MEMBER,
async execute(message, client, args) {
const authorFull = `${message.author.username}#${message.author.discriminator}`
const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null;
const attachmentExpires = message.attachments.size > 0 ? message.attachments.first().expires_at : null;
const reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires).catch(async (e) =>{await message.reply(e.message);});
if (typeof reply === 'string') {
return await message.reply(reply);
}
else if (reply instanceof EmbedBuilder) {
await message.reply({embeds: [reply.toJSON()]})
}
else if (typeof reply === 'object') {
const errorsText = reply.errors.length > 0 ? reply.errors.join('\n- ') : null;
return await message.reply({content: `${reply.success} ${errorsText ? "\nThese errors occurred:\n" + errorsText : ""}`, embeds: [reply.embed.toJSON()]})
}
async execute(message, args) {
await cmds.memberCommand(message, args)
}
})
cmds.set('help', {
/**
* Calls the member-related functions.
*
* @async
* @param {Message} message - The message object
* @param {string[]} args - The parsed arguments
*
**/
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;
let reply;
try {
reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires)
} catch (e) {
return await message.reply(e.message);
}
if (typeof reply === 'string') {
await message.reply(reply);
} else if (reply instanceof EmbedBuilder) {
await message.reply({embeds: [reply]})
} else if (typeof reply === 'object') {
// The little dash is so that the errors print out in bullet points in Fluxer
const errorsText = reply.errors.length > 0 ? '- ' + reply.errors.join('\n- ') : null;
return await message.reply({
content: `${reply.success} ${errorsText ? `\n\n${enums.err.ERRORS_OCCURRED}\n` + errorsText : ""}`,
embeds: [reply.embed]
})
}
}
cmds.commandsMap.set('help', {
description: enums.help.SHORT_DESC_HELP,
async execute(message) {
const fields = [...cmds.entries()].map(([name, cmd]) => ({
const fields = [...cmds.commandsMap.entries()].map(([name, cmd]) => ({
name: `${messageHelper.prefix}${name}`,
value: cmd.description,
inline: true,
@@ -40,39 +66,57 @@ cmds.set('help', {
.setTitle('Commands')
.setDescription(enums.help.PLURALFLUX)
.addFields(...fields)
.setFooter({ text: `Prefix: ${messageHelper.prefix}` })
.setFooter({text: `Prefix: ${messageHelper.prefix}`})
.setTimestamp();
await message.reply({ embeds: [embed.toJSON()] });
await message.reply({embeds: [embed]});
},
})
cmds.set('import', {
cmds.commandsMap.set('import', {
description: enums.help.SHORT_DESC_IMPORT,
async execute(message, client, args) {
const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null;
if ((message.content.includes('--help') || (args[0] === '' && args.length === 1)) && !attachmentUrl ) {
return await message.reply(enums.help.IMPORT);
}
return await importHelper.pluralKitImport(message.author.id, attachmentUrl).then(async (successfullyAdded) => {
await message.reply(successfullyAdded);
}).catch(async (error) => {
if (error instanceof AggregateError) {
// errors.message can be a list of successfully added members, or say that none were successful.
let errorsText = `${error.message}.\nThese errors occurred:\n${error.errors.join('\n')}`;
await message.reply(errorsText).catch(async () => {
const returnedBuffer = messageHelper.returnBufferFromText(errorsText);
await message.reply({content: returnedBuffer.text, files: [{ name: 'text.pdf', data: returnedBuffer.file }]
})
});
}
// If just one error was returned.
else {
return await message.reply(error.message);
}
})
async execute(message, args) {
await cmds.importCommand(message, args);
}
})
/**
* Calls the import-related functions.
*
* @async
* @param {Message} message - The message object
* @param {string[]} args - The parsed arguments
*
**/
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);
}
let errorsText;
try {
const successfullyAdded = await importHelper.pluralKitImport(message.author.id, attachmentUrl)
return await message.reply(successfullyAdded);
} catch (error) {
if (error instanceof AggregateError) {
// errors.message can be a list of successfully added members, or say that none were successful.
errorsText = `${error.message}.\n\n${enums.err.ERRORS_OCCURRED}\n\n${error.errors.join('\n')}`;
}
// If just one error was returned.
else {
console.error(error);
errorsText = error.message;
}
}
if (errorsText.length > 2000) {
const returnedBuffer = messageHelper.returnBufferFromText(errorsText);
await message.reply({
content: returnedBuffer.text, files: [{name: 'text.txt', data: returnedBuffer.file}]
})
} else {
await message.reply(errorsText)
}
}
export const commands = cmds;

View File

@@ -6,7 +6,7 @@ env.config();
const password = process.env.POSTGRES_PASSWORD;
if (!password) {
console.error("Missing POSTGRES_PWD environment variable.");
console.error("Missing POSTGRES_PASSWORD environment variable.");
process.exit(1);
}
@@ -59,26 +59,28 @@ db.systems = sequelize.define('System', {
/**
* Checks Sequelize database connection.
*/
db.check_connection = async function() {
await sequelize.authenticate().then(async () => {
console.log('Connection has been established successfully.');
await syncModels();
}).catch(err => {
console.error('Unable to connect to the database:', err);
process.exit(1);
});
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() {
await sequelize.sync().then(() => {
try {
await sequelize.sync()
console.log('Models synced successfully.');
}).catch((err) => {
} catch(err) {
console.error('Syncing models did not work', err);
process.exit(1);
});
}
}
export const database = db;

View File

@@ -3,15 +3,16 @@ const helperEnums = {};
helperEnums.err = {
NO_MEMBER: "No such member was found.",
NO_NAME_PROVIDED: "No member name was provided for",
NO_VALUE: "has not been set for this member. Please provide a value.",
NO_VALUE: "has not been set for this member.",
ADD_ERROR: "Error adding member.",
MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.",
USER_NO_MEMBERS: "You have no members created.",
NAME_REQUIRED: "You must set a unique name for the member for them to save.",
DISPLAY_NAME_TOO_LONG: "The maximum length of a display name is 32 characters.",
PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.",
NO_SUCH_COMMAND: "No such command exists.",
PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.",
PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.",
PROPIC_CANNOT_LOAD: "Profile picture could not be loaded. Are you sure this is a valid URL? (Try visiting the link to make sure!)",
NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.",
NOT_IN_SERVER: "You can only proxy in a server.",
NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.',
@@ -19,9 +20,10 @@ helperEnums.err = {
NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`",
NOT_JSON_FILE: "Please attach a valid JSON file.",
NO_MEMBERS_IMPORTED: 'No members were imported.',
IMPORT_ERROR: "Please see attached file for logs on the member import process.",
ERRORS_OCCURRED: "These errors occurred:",
COMMAND_NOT_RECOGNIZED: "Command not recognized. Try typing `pf;help` for command list.",
SET_TO_NULL: "It has been set to null instead."
SET_TO_NULL: "It has been set to null instead.",
CANNOT_FETCH_RESOURCE: "Could not download the file at this time."
}
helperEnums.help = {
@@ -30,15 +32,15 @@ helperEnums.help = {
SHORT_DESC_IMPORT: "Imports from PluralKit.",
SHORT_DESC_PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Type `pf;help` for info on the bot itself.",
PLURALFLUX: "PluralFlux is a proxybot akin to PluralKit and Tupperbot, but for Fluxer. All commands are prefixed by `pf;`. Add ` --help` to the end of a command to find out more about it, or just send it without arguments.",
MEMBER: "Accesses the sub-commands related to editing proxy members. The available subcommands are `list`, `new`, `remove`, `displayname`, `proxy`, and `propic`. Add ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.",
MEMBER: "Accesses the sub-commands related to adding, editing, and removing proxy members and the fields associated with them. Type `pf;member` and then the command name afterward to access it.\nAdd ` --help` to the end of a subcommand to find out more about it, or just send it without arguments.\nTo get information on a member, just write their name with no arguments afterward, for example: `pf;member jane`. To get the current value of a field instead of updating it, write without the last argument, for example: `pf;member jane displayname`; `pf;member jane propic`",
NEW: "Creates a new member to proxy with, for example: `pf;member new jane`. The member name should ideally be short so you can write other commands with it easily. \n\nThe order of values is `pf;member new [name] [displayname] [proxy] [propic]`, _without brackets_. The name is **required**, but the rest are optional.\nUsage notes:\n- If anything has spaces, put it in quotes.\n- If anything is unset and you want to set something after it (for ex: you haven't set a display name but you want to add a proxy), put the unset value in empty quotes in the same position: \"\" If you leave it out, the bot will set things wrong.\n- The maximum length of a display name is 32 characters.\n- You can't use the same proxy for two different members.\n- You can also upload an image directly instead of using a url.\nExamples:\n- Everything filled out: `pf;member new jane \"Jane Doe\" J:text https://cdn.pixabay.com/photo/2023/10/20/19/07/aster-8330078_1280.jpg`\n- Example with gaps: `pf;member new bob \"Bob he/him\" \"\" https://cdn.pixabay.com/photo/2016/05/09/11/09/tennis-1381230_1280.jpg`",
REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.",
LIST: "Lists members in the system. Currently only lists the first 25.",
LIST: "Lists members in the system. **Currently only lists the first 25.**",
NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.",
DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, put it in __double quotes__.",
PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**",
DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane displayname \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, __put it in quotes__.",
PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy A{text}` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**",
PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.",
IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**"
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."
}
helperEnums.misc = {

View File

@@ -8,36 +8,51 @@ const ih = {};
*
* @async
* @param {string} authorId - The author of the message
* @param {string} attachmentUrl - The attached JSON url.
* @returns {string} A successful addition of all members.
* @param {string | null} [attachmentUrl] - The attached JSON url.
* @returns {Promise<string>} A successful addition of all members.
* @throws {Error} When the member exists, or creating a member doesn't work.
*/
ih.pluralKitImport = async function (authorId, attachmentUrl) {
ih.pluralKitImport = async function (authorId, attachmentUrl= null) {
let fetchResult, pkData;
if (!attachmentUrl) {
throw new Error(enums.err.NOT_JSON_FILE);
}
return fetch(attachmentUrl).then((res) => res.json()).then(async(pkData) => {
const pkMembers = pkData.members;
let errors = [];
const addedMembers = [];
for (let pkMember of pkMembers) {
const proxy = pkMember.proxy_tags[0] ? `${pkMember.proxy_tags[0].prefix ?? ''}text${pkMember.proxy_tags[0].suffix ?? ''}` : null;
await memberHelper.addFullMember(authorId, pkMember.name, pkMember.display_name, proxy, pkMember.avatar_url).then((memberObj) => {
addedMembers.push(memberObj.member.name);
if (memberObj.errors.length > 0) {
errors.push(`\n**${pkMember.name}:** `)
errors = errors.concat(memberObj.errors);
}
}).catch(e => {
errors.push(e.message);
});
try {
fetchResult = await fetch(attachmentUrl);
}
catch(e) {
throw new Error(enums.err.CANNOT_FETCH_RESOURCE, { cause: e });
}
try {
pkData = await fetchResult.json();
}
catch(e) {
throw new Error(enums.err.NOT_JSON_FILE, { cause: e })
}
const pkMembers = pkData.members;
let errors = [];
let addedMembers = [];
for (let pkMember of pkMembers) {
const proxy = pkMember.proxy_tags[0] ? `${pkMember.proxy_tags[0].prefix ?? ''}text${pkMember.proxy_tags[0].suffix ?? ''}` : null;
try {
const memberObj = await memberHelper.addFullMember(authorId, pkMember.name, pkMember.display_name, proxy, pkMember.avatar_url);
addedMembers.push(memberObj.member.name);
if (memberObj.errors.length > 0) {
errors.push(`\n**${pkMember.name}:** `);
errors = errors.concat(memberObj.errors);
}
const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED;
if (errors.length > 0) {
throw new AggregateError(errors, aggregatedText);
}
return aggregatedText;
});
}
catch(e) {
errors.push(e.message);
}
}
const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : `${enums.err.NO_MEMBERS_IMPORTED}`;
if (errors.length > 0) {
throw new AggregateError(errors, aggregatedText);
}
return aggregatedText;
}
export const importHelper = ih;

View File

@@ -1,44 +1,149 @@
import {database} from '../database.js';
import {enums} from "../enums.js";
import {EmptyResultError, Op} from "sequelize";
import {Op} from "sequelize";
import {EmbedBuilder} from "@fluxerjs/core";
import {utils} from "./utils.js";
const mh = {};
// Has an empty "command" to parse the help message properly
const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', ''];
const commandList = ['new', 'remove', 'name', 'list', 'displayname', 'proxy', 'propic'];
const newAndRemoveCommands = ['new', 'remove'];
/**
* Parses through the subcommands that come after "pf;member" and calls functions accordingly.
* Parses through the subcommands that come after "pf;member" to identify member name, command, and associated values.
*
* @async
* @param {string} authorId - The id of the message author
* @param {string} authorFull - The username and discriminator of the message author
* @param {string[]} args - The message arguments
* @param {string | null} attachmentUrl - The message attachment url.
* @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer)
* @param {string | null} [attachmentUrl] - The attachment URL, if any
* @param {string | null} [attachmentExpiration] - The attachment expiry date, if any
* @returns {Promise<string>} A success message.
* @returns {Promise <EmbedBuilder>} A list of 25 members as an embed.
* @returns {Promise<{EmbedBuilder, [], string}>} A member info embed + info/errors.
* @throws {Error}
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
* @returns {Promise<{EmbedBuilder, string[], string}>} A member info embed + info/errors.
*/
mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) {
let member;
let memberName, command, isHelp = false;
// checks whether command is in list, otherwise assumes it's a name
if (!commandList.includes(args[0]) && !args[1]) {
member = await mh.getMemberInfo(authorId, args[0]);
// ex: pf;member remove, pf;member remove --help
// ex: pf;member, pf;member --help
if (args.length === 0 || args[0] === '--help' || args[0] === '') {
return mh.getMemberCommandInfo();
}
switch (args[0]) {
case '--help':
return enums.help.MEMBER;
// ex: pf;member remove somePerson
if (commandList.includes(args[0])) {
command = args[0];
if (args[1]) {
memberName = args[1];
}
}
// ex: pf;member somePerson propic
else if (args[1] && commandList.includes(args[1])) {
command = args[1];
memberName = args[0];
}
// ex: pf;member somePerson
else if (!commandList.includes(args[0]) && !args[1]) {
memberName = args[0];
}
if (args[1] === "--help" || command && (memberName === "--help" || !memberName && command !== 'list')) {
isHelp = true;
}
return await mh.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration)
}
/**
* Parses through the command, argument, and values and calls appropriate functions based on their presence or absence.
*
* @async
* @param {string} authorId - The id of the message author
* @param {string} authorFull - The username and discriminator of the message author
* @param {boolean} isHelp - Whether this is a help command or not
* @param {string | null} [command] - The command name
* @param {string | null} [memberName] - The member name
* @param {string[]} [args] - The message arguments
* @param {string | null} [attachmentUrl] - The attachment URL, if any
* @param {string | null} [attachmentExpiration] - The attachment expiry date, if any
* @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.
* @throws {Error} When there's no member or a command is not recognized.
*/
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 mh.sendHelpEnum(command);
}
else if (command === "list") {
return await mh.getAllMembersInfo(authorId, authorFull);
}
else if (!memberName && !isHelp) {
throw new Error(enums.err.NO_MEMBER);
}
// remove memberName and command from values to reduce confusion
const values = args.slice(2);
// ex: pf;member blah blah
if (command && memberName && (values.length > 0 || newAndRemoveCommands.includes(command) || attachmentUrl)) {
return await mh.memberCommandHandler(authorId, command, memberName, values, attachmentUrl, attachmentExpiration);
}
else if (memberName && values.length === 0) {
return await mh.sendCurrentValue(authorId, memberName, command);
}
}
/**
* Sends the current value of a field based on the command.
*
* @async
* @param {string} authorId - The id of the message author
* @param {string} memberName - The name of the member
* @param {string | null} [command] - The command being called to query a value.
* @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.
* @throws {Error} When there's no member
*/
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 mh.getMemberInfo(member);
}
switch (command) {
case 'name':
return `The name of ${member.name} is \"${member.name}\" but you probably already knew that!`;
case 'displayname':
return member.displayname ? `The display name for ${member.name} is \"${member.displayname}\".` : `Display name ${enums.err.NO_VALUE}`;
case 'proxy':
return member.proxy ? `The proxy for ${member.name} is \"${member.proxy}\".` : `Proxy ${enums.err.NO_VALUE}`;
case 'propic':
return member.propic ? `The profile picture for ${member.name} is \"${member.propic}\".` : `Propic ${enums.err.NO_VALUE}`;
}
}
/**
* Sends the help text associated with a command.
*
* @param {string} command - The command being called.
* @returns {string} - The help text associated with a command.
*/
mh.sendHelpEnum = function(command) {
switch (command) {
case 'new':
return await mh.addNewMember(authorId, args, attachmentUrl).catch((e) => {
throw e
});
return enums.help.NEW;
case 'remove':
return await mh.removeMember(authorId, args).catch((e) => {
throw e
});
return enums.help.REMOVE;
case 'name':
return enums.help.NAME;
case 'displayname':
@@ -48,37 +153,41 @@ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUr
case 'propic':
return enums.help.PROPIC;
case 'list':
if (args[1] && args[1] === "--help") {
return enums.help.LIST;
}
return await mh.getAllMembersInfo(authorId, authorFull).catch((e) => {
throw e
});
case '':
return enums.help.MEMBER;
return enums.help.LIST;
}
switch (args[1]) {
}
/**
* Handles the commands that need to call other update/edit commands.
*
* @async
* @param {string} authorId - The id of the message author
* @param {string} memberName - The name of the member
* @param {string} command - The command being called.
* @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>} 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.
*/
mh.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
switch (command) {
case 'new':
return await mh.addNewMember(authorId, memberName, values, attachmentUrl, attachmentExpiration);
case 'remove':
return await mh.removeMember(authorId, memberName);
case 'name':
return await mh.updateName(authorId, args).catch((e) => {
throw e
});
return await mh.updateName(authorId, memberName, values[0]);
case 'displayname':
return await mh.updateDisplayName(authorId, args).catch((e) => {
throw e
});
return await mh.updateDisplayName(authorId, memberName, values[0]);
case 'proxy':
if (!args[2]) return await mh.getProxyByMember(authorId, args[0]).catch((e) => {
throw e
});
return await mh.updateProxy(authorId, args).catch((e) => {
throw e
});
return await mh.updateProxy(authorId, memberName, values[0]);
case 'propic':
return await mh.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) => {
throw e
});
return await mh.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration);
default:
return member;
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
}
}
@@ -87,26 +196,20 @@ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUr
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @param {string | null} attachmentURL - The attachment URL, if any exists
* @returns {Promise<string>} A successful addition.
* @throws {Error} When the member exists, or creating a member doesn't work.
* @param {string} memberName - The member name
* @param {string[]} values - The arguments following the member name and command
* @param {string | null} [attachmentUrl] - The attachment URL, if any
* @param {string | null} [attachmentExpiration] - The attachment expiry date, if any
* @returns {Promise<{EmbedBuilder, string[], string}>} A successful addition.
*/
mh.addNewMember = async function (authorId, args, attachmentURL = null) {
if (args[1] && args[1] === "--help" || !args[1]) {
return enums.help.NEW;
}
const memberName = args[1];
const displayName = args[2];
const proxy = args[3];
const propic = args[4] ?? attachmentURL;
mh.addNewMember = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
const displayName = values[0];
const proxy = values[1];
const propic = values[2] ?? attachmentUrl;
return await mh.addFullMember(authorId, memberName, displayName, proxy, propic).then(async(response) => {
const memberInfoEmbed = await mh.getMemberInfo(authorId, memberName).catch((e) => {throw e})
return {embed: memberInfoEmbed, errors: response.errors, success: `${memberName} has been added successfully.`};
}).catch(e => {
throw e;
})
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.`}
}
/**
@@ -114,26 +217,17 @@ mh.addNewMember = async function (authorId, args, attachmentURL = null) {
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @param {string} memberName - The member to update
* @param {string} name - The message arguments
* @returns {Promise<string>} A successful update.
* @throws {RangeError} When the name doesn't exist.
*/
mh.updateName = async function (authorId, args) {
if (args[2] && args[2] === "--help") {
return enums.help.NAME;
}
const name = args[2];
if (!name) {
return `The name for ${args[0]} is ${args[0]}, but you probably knew that!`;
}
mh.updateName = async function (authorId, memberName, name) {
const trimmedName = name.trim();
if (trimmedName === '') {
throw new RangeError(`Name ${enums.err.NO_VALUE}`);
}
return await mh.updateMemberField(authorId, args).catch((e) => {
throw e
});
return await mh.updateMemberField(authorId, memberName, "name", trimmedName);
}
/**
@@ -141,36 +235,21 @@ mh.updateName = async function (authorId, args) {
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @param {string} membername - The member to update
* @param {string} displayname - The display name to set
* @returns {Promise<string>} A successful update.
* @throws {RangeError} When the display name is too long or doesn't exist.
*/
mh.updateDisplayName = async function (authorId, args) {
if (args[2] && args[2] === "--help") {
return enums.help.DISPLAY_NAME;
}
mh.updateDisplayName = async function (authorId, membername, displayname) {
const trimmedName = displayname.trim();
const memberName = args[0];
const displayName = args[2];
const trimmedName = displayName ? displayName.trim() : null;
if (!displayName) {
return await mh.getMemberByName(authorId, memberName).then((member) => {
if (member && member.displayname) {
return `Display name for ${memberName} is: \"${member.displayname}\".`;
} else if (member) {
throw new Error(`Display name ${enums.err.NO_VALUE}`);
}
});
} else if (displayName.length > 32) {
if (trimmedName.length > 32) {
throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG);
}
else if (trimmedName === '') {
throw new RangeError(`Display name ${enums.err.NO_VALUE}`);
}
return await mh.updateMemberField(authorId, args).catch((e) => {
throw e
});
return await mh.updateMemberField(authorId, membername, "displayname", trimmedName);
}
/**
@@ -178,24 +257,15 @@ mh.updateDisplayName = async function (authorId, args) {
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @param {string} memberName - The member to update
* @param {string} proxy - The proxy to set
* @returns {Promise<string> } A successful update.
* @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists.
*/
mh.updateProxy = async function (authorId, args) {
if (args[2] && args[2] === "--help") {
return enums.help.PROXY;
}
const proxyExists = await mh.checkIfProxyExists(authorId, args[2]).then((proxyExists) => {
return proxyExists;
}).catch((e) => {
throw e
});
if (!proxyExists) {
return await mh.updateMemberField(authorId, args).catch((e) => {
throw e
});
}
mh.updateProxy = async function (authorId, memberName, proxy) {
// Throws error if exists
await mh.checkIfProxyExists(authorId, proxy);
return await mh.updateMemberField(authorId, memberName, "proxy", proxy);
}
/**
@@ -203,53 +273,18 @@ mh.updateProxy = async function (authorId, args) {
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @param {string} attachmentUrl - The url of the first attachment in the message
* @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer)
* @param {string} memberName - The member to update
* @param {string} values - The message arguments
* @param {string | null} attachmentUrl - The attachment URL, if any
* @param {string | null} attachmentExpiration - The attachment expiry date, if any
* @returns {Promise<string>} A successful update.
* @throws {Error} When loading the profile picture from a URL doesn't work.
*/
mh.updatePropic = async function (authorId, args, attachmentUrl, attachmentExpiry = null) {
if (args[2] && args[2] === "--help") {
return enums.help.PROPIC;
}
let img;
const updatedArgs = args;
if (!updatedArgs[1] && !attachmentUrl) {
return enums.help.PROPIC;
} else if (attachmentUrl) {
updatedArgs[2] = attachmentUrl;
updatedArgs[3] = attachmentExpiry;
}
if (updatedArgs[2]) {
img = updatedArgs[2];
}
const isValidImage = await mh.checkImageFormatValidity(img).catch((e) => {
throw e
});
if (isValidImage) {
return await mh.updateMemberField(authorId, updatedArgs).catch((e) => {
throw e
});
}
}
mh.updatePropic = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
const imgUrl = values ?? attachmentUrl;
// Throws error if invalid
await utils.checkImageFormatValidity(imgUrl);
/**
* Checks if an uploaded picture is in the right format.
*
* @async
* @param {string} imageUrl - The url of the image
* @returns {Promise<boolean>} - If the image is a valid format.
* @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements.
*/
mh.checkImageFormatValidity = async function (imageUrl) {
const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp'];
return await fetch(imageUrl).then(r => r.blob()).then(blobFile => {
if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS);
return true;
}).catch((error) => {
throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`);
});
return await mh.updateMemberField(authorId, memberName, "propic", imgUrl, attachmentExpiration);
}
/**
@@ -257,27 +292,22 @@ mh.checkImageFormatValidity = async function (imageUrl) {
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @param {string} memberName - The name of the member to remove
* @returns {Promise<string>} A successful removal.
* @throws {EmptyResultError} When there is no member to remove.
* @throws {Error} When there is no member to remove.
*/
mh.removeMember = async function (authorId, args) {
if (args[1] && args[1] === "--help" || !args[1]) {
return enums.help.REMOVE;
}
const memberName = args[1];
return await database.members.destroy({
mh.removeMember = async function (authorId, memberName) {
const destroyed = await database.members.destroy({
where: {
name: {[Op.iLike]: memberName},
userid: authorId
}
}).then((result) => {
if (result) {
return `Member "${memberName}" has been deleted.`;
}
throw new EmptyResultError(`${enums.err.NO_MEMBER}`);
})
if (destroyed > 0) {
return `Member "${memberName}" has been deleted.`;
} else {
throw new Error(`${enums.err.NO_MEMBER}`);
}
}
/*======Non-Subcommands======*/
@@ -288,24 +318,33 @@ mh.removeMember = async function (authorId, args) {
* @async
* @param {string} authorId - The author of the message
* @param {string} memberName - The name of the member.
* @param {string | null} displayName - The display name of the member.
* @param {string | null} proxy - The proxy tag of the member.
* @param {string | null} propic - The profile picture URL of the member.
* @returns {Promise<{model, []}>} A successful addition object, including errors if there are any.
* @param {string | null} [displayName] - The display name of the member.
* @param {string | null} [proxy] - The proxy tag of the member.
* @param {string | null} [propic] - The profile picture URL of the member.
* @param {string | null} [attachmentExpiration] - The expiration date of an uploaded profile picture.
* @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.
*/
mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null) {
await mh.getMemberByName(authorId, memberName).then((member) => {
if (member) {
throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`);
}
});
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}`);
}
const errors = [];
const trimmedName = memberName.trim();
if (trimmedName.length === 0) {
throw new Error(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`);
}
let isValidDisplayName;
if (displayName && displayName.length > 0) {
const trimmedName = displayName ? displayName.trim() : null;
if (trimmedName && trimmedName.length > 32) {
if (displayName) {
const trimmedDisplayName= displayName ? displayName.trim() : null;
if (!trimmedDisplayName || trimmedDisplayName.length === 0) {
errors.push(`Display name ${enums.err.NO_VALUE}. ${enums.err.SET_TO_NULL}`);
isValidDisplayName = false;
}
else if (trimmedDisplayName.length > 32) {
errors.push(`Tried to set displayname to \"${displayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`);
isValidDisplayName = false;
}
@@ -316,22 +355,29 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro
let isValidProxy;
if (proxy && proxy.length > 0) {
await mh.checkIfProxyExists(authorId, proxy).then(() => {
isValidProxy = true;
}).catch((e) => {
try {
const proxyExists = await mh.checkIfProxyExists(authorId, proxy);
isValidProxy = !proxyExists;
}
catch(e) {
errors.push(`Tried to set proxy to \"${proxy}\". ${e.message}. ${enums.err.SET_TO_NULL}`);
isValidProxy = false;
});
}
}
let isValidPropic;
if (propic && propic.length > 0) {
await mh.checkImageFormatValidity(propic).then(() => {
isValidPropic = true;
}).catch((e) => {
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 member = await database.members.create({
name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
@@ -340,139 +386,36 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro
return {member: member, errors: errors};
}
// mh.mergeFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null) {
// await mh.getMemberByName(authorId, memberName).then((member) => {
// if (member) {
// throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`);
// }
// });
//
// let isValidDisplayName;
// if (displayName) {
// const trimmedName = displayName ? displayName.trim() : null;
// if (trimmedName && trimmedName.length > 32) {
// if (!isImport) {
// throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`);
// }
// isValidDisplayName = false;
// }
// }
//
// let isValidProxy;
// if (proxy) {
// isValidProxy = await mh.checkIfProxyExists(authorId, proxy).then((res) => {
// return res;
// }).catch((e) => {
// if (!isImport) {
// throw e
// }
// return false;
// });
// }
//
// let isValidPropic;
// if (propic) {
// isValidPropic = await mh.checkImageFormatValidity(propic).then((valid) => {
// return valid;
// }).catch((e) => {
// if (!isImport) {
// throw (e);
// }
// return false;
// });
// }
//
// const member = await database.members.create({
// name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName: null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null,
// });
// if (!member) {
// new Error(`${enums.err.ADD_ERROR}`);
// }
// return member;
// }
//
// mh.overwriteFullMemberFromImport = async function (authorId, memberName, displayName = null, proxy = null, propic = null) {
// await mh.getMemberByName(authorId, memberName).then((member) => {
// if (member) {
// throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`);
// }
// });
//
// let isValidDisplayName;
// if (displayName) {
// const trimmedName = displayName ? displayName.trim() : null;
// if (trimmedName && trimmedName.length > 32) {
// if (!isImport) {
// throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`);
// }
// isValidDisplayName = false;
// }
// }
//
// let isValidProxy;
// if (proxy) {
// isValidProxy = await mh.checkIfProxyExists(authorId, proxy).then((res) => {
// return res;
// }).catch((e) => {
// if (!isImport) {
// throw e
// }
// return false;
// });
// }
//
// let isValidPropic;
// if (propic) {
// isValidPropic = await mh.checkImageFormatValidity(propic).then((valid) => {
// return valid;
// }).catch((e) => {
// if (!isImport) {
// throw (e);
// }
// return false;
// });
// }
//
// const member = await database.members.create({
// name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName: null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null,
// });
// if (!member) {
// new Error(`${enums.err.ADD_ERROR}`);
// }
// return member;
// }
/**
* Updates one fields for a member in the database.
*
* @async
* @param {string} authorId - The author of the message
* @param {string[]} args - The message arguments
* @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)
* @returns {Promise<string>} A successful update.
* @throws {EmptyResultError | Error} When the member is not found, or catchall error.
* @throws {Error} When no member row was updated.
*/
mh.updateMemberField = async function (authorId, args) {
const memberName = args[0];
const columnName = args[1];
const value = args[2];
mh.updateMemberField = async function (authorId, memberName, columnName, value, attachmentExpiration = null) {
let fluxerPropicWarning;
// indicates that an attachment was uploaded on Fluxer directly
if (columnName === "propic" && args[3]) {
fluxerPropicWarning = mh.setExpirationWarning(args[3]);
if (columnName === "propic" && attachmentExpiration) {
fluxerPropicWarning = mh.setExpirationWarning(value);
}
return await database.members.update({[columnName]: value}, {
const res = await database.members.update({[columnName]: value}, {
where: {
name: {[Op.iLike]: memberName},
userid: authorId
}
}).then((res) => {
if (res[0] === 0) {
throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
} else {
return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`;
}
})
if (res[0] === 0) {
throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
} else {
return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`;
}
}
/**
@@ -492,25 +435,19 @@ mh.setExpirationWarning = function (expirationString) {
/**
* Gets the details for a member.
*
* @async
* @param {string} authorId - The author of the message
* @param {string} memberName - The message arguments
* @returns {Promise<EmbedBuilder>} The member's info.
* @param {model} member - The member object
* @returns {EmbedBuilder} The member's info.
*/
mh.getMemberInfo = async function (authorId, memberName) {
return await mh.getMemberByName(authorId, memberName).then((member) => {
if (member) {
return new EmbedBuilder()
.setTitle(member.name)
.setDescription(`Details for ${member.name}`)
.addFields({
name: 'Display name: ',
value: member.displayname ?? 'unset',
inline: true
}, {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true},)
.setImage(member.propic);
}
});
mh.getMemberInfo = function (member) {
return new EmbedBuilder()
.setTitle(member.name)
.setDescription(`Details for ${member.name}`)
.addFields({
name: 'Display name: ',
value: member.displayname ?? 'unset',
inline: true
}, {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true},)
.setImage(member.propic ?? null);
}
/**
@@ -524,12 +461,12 @@ mh.getMemberInfo = async function (authorId, memberName) {
*/
mh.getAllMembersInfo = async function (authorId, authorName) {
const members = await mh.getMembersByAuthor(authorId);
if (members == null) throw Error(enums.err.USER_NO_MEMBERS);
const fields = [...members.entries()].map(([name, member]) => ({
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,
}));
return new EmbedBuilder()
.setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`)
.setTitle(`${fields.length > 25 ? "First 25 m" : "M"}embers for ${authorName}`)
.addFields(...fields);
}
@@ -540,42 +477,11 @@ mh.getAllMembersInfo = async function (authorId, authorName) {
* @param {string} authorId - The author of the message.
* @param {string} memberName - The member's name.
* @returns {Promise<model>} The member object.
* @throws { EmptyResultError } When the member is not found.
*/
mh.getMemberByName = async function (authorId, memberName) {
return await database.members.findOne({where: {userid: authorId, name: {[Op.iLike]: memberName}}});
}
/**
* Gets a member based on the author and proxy tag.
*
* @async
* @param {string} authorId - The author of the message.
* @param {string} memberName - The member's name.
* @returns {Promise<string>} The member object.
* @throws { EmptyResultError } When the member is not found.
*/
mh.getProxyByMember = async function (authorId, memberName) {
return await mh.getMemberByName(authorId, memberName).then((member) => {
if (member) {
return member.proxy;
}
throw new EmptyResultError(enums.err.NO_MEMBER);
})
}
/**
* Gets a member based on the author and proxy tag.
*
* @async
* @param {string} authorId - The author of the message
* @param {string} proxy - The proxy tag
* @returns {Promise<model>} The member object.
*/
mh.getMemberByProxy = async function (authorId, proxy) {
return await db.members.findOne({where: {userid: authorId, proxy: proxy}});
}
/**
* Gets all members belonging to the author.
*
@@ -587,7 +493,6 @@ mh.getMembersByAuthor = async function (authorId) {
return await database.members.findAll({where: {userid: authorId}});
}
/**
* Checks if proxy exists for a member.
*
@@ -597,21 +502,37 @@ mh.getMembersByAuthor = async function (authorId) {
* @throws {Error} When an empty proxy was provided, or no proxy exists.
*/
mh.checkIfProxyExists = async function (authorId, proxy) {
if (proxy) {
const splitProxy = proxy.trim().split("text");
if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY);
if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER);
const splitProxy = proxy.trim().split("text");
if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY);
if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER);
await mh.getMembersByAuthor(authorId).then((memberList) => {
const proxyExists = memberList.some(member => member.proxy === proxy);
if (proxyExists) {
throw new Error(enums.err.PROXY_EXISTS);
}
}).catch(e => {
throw e
});
const memberList = await mh.getMembersByAuthor(authorId);
const proxyExists = memberList.some(member => member.proxy === proxy);
if (proxyExists) {
throw new Error(enums.err.PROXY_EXISTS);
}
return false;
}
/**
* Creates an embed with all member commands
*
* @returns {EmbedBuilder } An embed of member commands.
*/
mh.getMemberCommandInfo = function() {
const fields = [
{name: `**new**`, value: enums.help.NEW, inline: false},
{name: `**remove**`, value: enums.help.REMOVE, inline: false},
{name: `**name**`, value: enums.help.NAME, inline: false},
{name: `**displayname**`, value: enums.help.DISPLAY_NAME, inline: false},
{name: `**proxy**`, value: enums.help.PROXY, inline: false},
{name: `**propic**`, value: enums.help.PROPIC, inline: false},
{name: `**list**`, value: enums.help.LIST, inline: false},
];
return new EmbedBuilder()
.setTitle("Member subcommands")
.setDescription(enums.help.MEMBER)
.addFields(...fields);
}

View File

@@ -1,15 +1,9 @@
import {memberHelper} from "./memberHelper.js";
import {enums} from "../enums.js";
import tmp, {setGracefulCleanup} from "tmp";
import fs from 'fs';
import {Message} from "@fluxerjs/core";
const msgh = {};
msgh.prefix = "pf;"
setGracefulCleanup();
/**
* Parses and slices up message arguments, retaining quoted strings.
*
@@ -38,11 +32,11 @@ msgh.parseCommandArgs = function(content, commandName) {
/**
* Parses messages to see if any part of the text matches the tags of any member belonging to an author.
*
* @async
* @param {string} authorId - The author of the message.
* @param {string} content - The full message content
* @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists.
* @returns {{model, string, bool}} The proxy message object.
* @throws {Error} If a proxy message is sent with no message within it.
* @param {string | null} [attachmentUrl] - The url for an attachment to the message, if any exists.
* @returns {Promise<{model, string, bool}>} The proxy message object.
*/
msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){
const members = await memberHelper.getMembersByAuthor(authorId);

37
src/helpers/utils.js Normal file
View File

@@ -0,0 +1,37 @@
import {enums} from '../enums.js'
const u = {};
u.debounce = function(func, delay) {
let timeout = null;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
}
/**
* Checks if an uploaded picture is in the right format.
*
* @async
* @param {string} imageUrl - The url of the image
* @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.
*/
u.checkImageFormatValidity = async function (imageUrl) {
const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp'];
let response, blobFile;
try {
response = await fetch(imageUrl);
}
catch(e) {
throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${e.message}`);
}
blobFile = await response.blob();
if (blobFile.size > 10000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS);
return true;
}
export const utils = u;

View File

@@ -15,7 +15,7 @@ const name = 'PluralFlux Proxy Webhook';
*/
wh.sendMessageAsMember = async function(client, message) {
const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null;
const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e});
const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl);
// If the message doesn't match a proxy, just return.
if (!proxyMatch || !proxyMatch.member || (proxyMatch.message.length === 0 && !proxyMatch.hasAttachment) ) {
return;
@@ -27,7 +27,7 @@ wh.sendMessageAsMember = async function(client, message) {
if (proxyMatch.hasAttachment) {
return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname ?? proxyMatch.member.name}`)
}
await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e});
await wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member);
}
/**
@@ -43,20 +43,19 @@ 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 wh.getOrCreateWebhook(client, channel).catch((e) =>{throw e});
const webhook = await wh.getOrCreateWebhook(client, channel);
const username = member.displayname ?? member.name;
if (text.length > 0) {
await webhook.send({content: text, username: username, avatar_url: member.propic}).catch(async(e) => {
const returnedBuffer = messageHelper.returnBufferFromText(text);
await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.txt', data: returnedBuffer.file }]
})
console.error(e);
});
if (text.length <= 2000) {
await webhook.send({content: text, username: username, avatar_url: member.propic})
}
else if (text.length > 2000) {
const returnedBuffer = messageHelper.returnBufferFromText(text);
await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.txt', data: returnedBuffer.file }]
})
}
if (message.attachments.size > 0) {
// Not implemented yet
}
await message.delete();
}
}
@@ -72,7 +71,7 @@ wh.replaceMessage = async function(client, message, text, member) {
wh.getOrCreateWebhook = async function(client, channel) {
// If channel doesn't allow webhooks
if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED);
let webhook = await wh.getWebhook(client, channel).catch((e) =>{throw e});
let webhook = await wh.getWebhook(client, channel)
if (!webhook) {
webhook = await channel.createWebhook({name: name});
}
@@ -91,13 +90,7 @@ wh.getWebhook = async function(client, channel) {
if (channelWebhooks.length === 0) {
return;
}
let pf_webhook;
channelWebhooks.forEach((webhook) => {
if (webhook.name === name) {
pf_webhook = webhook;
}
})
return pf_webhook;
return channelWebhooks.find((webhook) => webhook.name === name);
}
export const webhookHelper = wh;

291
tests/bot.test.js Normal file
View File

@@ -0,0 +1,291 @@
const env = require('dotenv').config({path: './.env.jest'})
const {enums} = require("../src/enums.js");
jest.mock('@fluxerjs/core', () => {
return {
Events: {
MessageCreate: jest.fn(),
Ready: jest.fn(),
GuildCreate: jest.fn(),
},
Client: jest.fn().mockImplementation(() => {
return {
on: jest.fn(),
intents: 0,
login: jest.fn()
}
}),
Message: jest.fn()
};
});
jest.mock("../src/helpers/messageHelper.js", () => {
return {
messageHelper: {
parseCommandArgs: jest.fn(),
prefix: "pf;"
}
}
});
jest.mock("../src/helpers/webhookHelper.js", () => {
return {
webhookHelper: {
sendMessageAsMember: jest.fn()
}
}
})
jest.mock("../src/helpers/utils.js", () => {
return {
utils: {
debounce: jest.fn()
}
}
})
jest.mock("../src/commands.js", () => {
return {
commands: {
commandsMap: {
get: jest.fn(),
},
aliasesMap: {
get: jest.fn()
}
}
}
})
const {Client, Events} = require('@fluxerjs/core');
const {messageHelper} = require("../src/helpers/messageHelper.js");
const {commands} = require("../src/commands.js");
const {webhookHelper} = require("../src/helpers/webhookHelper.js");
const {utils} = require("../src/helpers/utils.js");
let {handleMessageCreate, client} = require("../src/bot.js");
const {login} = require("../src/bot");
describe('bot', () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
})
describe('handleMessageCreate', () => {
test('on message creation, if message is from bot, return', async () => {
// Arrange
const message = {
author: {
bot: true
}
}
// Act
const res = await handleMessageCreate(message);
expect(res).toBeUndefined();
})
test("if message doesn't start with bot prefix, call sendMessageAsMember", async () => {
// Arrange
webhookHelper.sendMessageAsMember.mockResolvedValue();
const message = {
content: "hello",
author: {
bot: false
}
}
// Act
const res = await handleMessageCreate(message);
// Assert
expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledTimes(1);
expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledWith(client, message)
})
test("if sendMessageAsMember returns error, catch and log error", async () => {
// Arrange
webhookHelper.sendMessageAsMember.mockRejectedValue(new Error("error"));
const message = {
content: "hello",
author: {
bot: false
}
}
jest.spyOn(global.console, 'error').mockImplementation(() => {});
// Act
await handleMessageCreate(message);
// Assert
expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledTimes(1);
expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledWith(client, message)
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(new Error('error'));
})
test("if no command after prefix, return correct enum", async () => {
// Arrange
const message = {
content: "pf;",
author: {
bot: false
},
reply: jest.fn()
}
// Act
await handleMessageCreate(message);
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(enums.help.SHORT_DESC_PLURALFLUX);
expect(webhookHelper.sendMessageAsMember).not.toHaveBeenCalled();
})
test("if command after prefix, call parseCommandArgs and commandsMap.get", async () => {
// Arrange
const message = {
content: "pf;help",
author: {
bot: false
},
reply: jest.fn()
}
const command = {
execute: jest.fn().mockResolvedValue(),
}
commands.commandsMap.get = jest.fn().mockReturnValue(command);
// Act
await handleMessageCreate(message);
// Assert
expect(messageHelper.parseCommandArgs).toHaveBeenCalledTimes(1);
expect(messageHelper.parseCommandArgs).toHaveBeenCalledWith('pf;help', 'help');
expect(commands.commandsMap.get).toHaveBeenCalledTimes(1);
expect(commands.commandsMap.get).toHaveBeenCalledWith('help');
expect(webhookHelper.sendMessageAsMember).not.toHaveBeenCalled();
})
test('if commands.commandsMap.get returns undefined, call aliasesMap.get and commandsMap.get again with that value', async () => {
// Arrange
const message = {
content: "pf;m",
author: {
bot: false
},
reply: jest.fn()
}
const mockAlias = {
command: 'member'
}
commands.commandsMap.get = jest.fn().mockReturnValueOnce();
commands.aliasesMap.get = jest.fn().mockReturnValueOnce(mockAlias);
// Act
await handleMessageCreate(message);
// Assert
expect(commands.commandsMap.get).toHaveBeenCalledTimes(2);
expect(commands.commandsMap.get).toHaveBeenNthCalledWith(1, 'm');
expect(commands.commandsMap.get).toHaveBeenNthCalledWith(2, 'member');
expect(commands.aliasesMap.get).toHaveBeenCalledTimes(1);
expect(commands.aliasesMap.get).toHaveBeenCalledWith('m');
})
test('if aliasesMap.get returns undefined, do not call commandsMap again', async () => {
// Arrange
const message = {
content: "pf;m",
author: {
bot: false
},
reply: jest.fn()
}
const mockAlias = {
command: 'member'
}
commands.commandsMap.get = jest.fn().mockReturnValueOnce();
commands.aliasesMap.get = jest.fn().mockReturnValueOnce();
// Act
await handleMessageCreate(message);
// Assert
expect(commands.aliasesMap.get).toHaveBeenCalledTimes(1);
expect(commands.aliasesMap.get).toHaveBeenCalledWith('m');
})
test("if command exists, call command.execute", async () => {
// Arrange
const message = {
content: "pf;member test",
author: {
bot: false
},
reply: jest.fn()
}
const command = {
execute: jest.fn()
}
messageHelper.parseCommandArgs = jest.fn().mockReturnValue(['test']);
commands.commandsMap.get = jest.fn().mockReturnValue(command);
command.execute = jest.fn().mockResolvedValue();
// Act
await handleMessageCreate(message)
// Assert
expect(command.execute).toHaveBeenCalledTimes(1);
expect(command.execute).toHaveBeenCalledWith(message, ['test']);
expect(webhookHelper.sendMessageAsMember).not.toHaveBeenCalled();
});
})
test("if command.execute returns error, log error", async () => {
// Arrange
const command = {
execute: jest.fn()
}
commands.commandsMap.get = jest.fn().mockReturnValue(command);
command.execute.mockRejectedValue(new Error("error"));
const message = {
content: "pf;member test",
author: {
bot: false
},
reply: jest.fn()
}
jest.spyOn(global.console, 'error').mockImplementation(() => {
})
// Act
await handleMessageCreate(message);
// Assert
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(new Error('error'))
})
test("if command does not exist, return correct enum", async () => {
// Arrange
commands.commandsMap.get = jest.fn().mockReturnValue();
commands.aliasesMap.get = jest.fn().mockReturnValue();
const message = {
content: "pf;asdfjlas",
author: {
bot: false
},
reply: jest.fn()
}
// Act
await handleMessageCreate(message);
// Assert
expect(message.reply).toHaveBeenCalledWith(enums.err.COMMAND_NOT_RECOGNIZED);
expect(message.reply).toHaveBeenCalledTimes(1);
})
test('login calls client.login with correct argument', async () => {
// Arrange
client.login = jest.fn().mockResolvedValue();
// Act
await login();
// Assert
expect(client.login).toHaveBeenCalledTimes(1);
expect(client.login).toHaveBeenCalledWith(process.env.FLUXER_BOT_TOKEN)
})
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
})

210
tests/commands.test.js Normal file
View File

@@ -0,0 +1,210 @@
import {enums} from "../src/enums.js";
jest.mock("../src/helpers/messageHelper.js", () => {
return {
messageHelper: {
returnBufferFromText: jest.fn(),
prefix: 'pf;'
}
}
})
jest.mock('../src/helpers/memberHelper.js', () => {
return {
memberHelper: {
parseMemberCommand: jest.fn()
}
}
})
jest.mock('../src/helpers/importHelper.js', () => {
return {
importHelper: {
pluralKitImport: jest.fn()
}
}
})
import {messageHelper} from "../src/helpers/messageHelper.js";
import {memberHelper} from "../src/helpers/memberHelper.js";
import {EmbedBuilder} from "@fluxerjs/core";
import {importHelper} from "../src/helpers/importHelper.js";
import {commands} from "../src/commands.js";
describe('commands', () => {
const authorId = '123';
const discriminator = '123';
const username = 'somePerson'
const attachmentUrl = 'oya.json';
const attachmentExpiration = new Date('2026-01-01').toDateString();
let message;
const args = ['new']
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
message = {
author: {
username: username,
id: authorId,
discriminator: discriminator,
},
attachments: {
size: 1,
first: jest.fn().mockImplementation(() => {
return {
url: attachmentUrl,
expires_at: attachmentExpiration
}
})
},
reply: jest.fn().mockResolvedValue(),
content: 'pf;import'
}
})
describe('memberCommand', () => {
test('calls parseMemberCommand with the correct arguments', async () => {
// Arrange
memberHelper.parseMemberCommand = jest.fn().mockResolvedValue("parsed command");
// Act
await commands.memberCommand(message, args)
// Assert
expect(memberHelper.parseMemberCommand).toHaveBeenCalledTimes(1);
expect(memberHelper.parseMemberCommand).toHaveBeenCalledWith(authorId, `${username}#${discriminator}`, args, attachmentUrl, attachmentExpiration);
});
})
test('if parseMemberCommand returns error, log error and reply with error', async () => {
// Arrange
memberHelper.parseMemberCommand = jest.fn().mockRejectedValue(new Error('error'));
// Act
await commands.memberCommand(message, args)
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith('error');
});
test('if parseMemberCommand returns embed, reply with embed', async () => {
// Arrange
const embed = new EmbedBuilder();
memberHelper.parseMemberCommand = jest.fn().mockResolvedValue(embed);
// Act
await commands.memberCommand(message, args);
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith({embeds: [embed]})
})
test('if parseMemberCommand returns object, reply with embed and content', async () => {
// Arrange
const reply = {
errors: ['error', 'error2'],
success: 'success',
embed: {title: 'hi'}
}
const expected = {
content: `success \n\n${enums.err.ERRORS_OCCURRED}\n- error\n- error2`,
embeds: [reply.embed]
}
console.log(expected)
memberHelper.parseMemberCommand = jest.fn().mockResolvedValue(reply);
// Act
await commands.memberCommand(message, args);
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(expected)
})
describe('importCommand', () => {
test('if message includes --help and no attachmentURL, return help message', async () => {
// Arrange
const args = ["--help"];
message.content = "pf;import --help";
message.attachments.size = 0;
// Act
await commands.importCommand(message, args)
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(enums.help.IMPORT);
expect(importHelper.pluralKitImport).not.toHaveBeenCalled();
})
test('if no args and no attachmentURL, return help message', async () => {
// Arrange
const args = [""];
message.content = 'pf;import'
message.attachments.size = 0;
// Act
await commands.importCommand(message, args)
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(enums.help.IMPORT);
expect(importHelper.pluralKitImport).not.toHaveBeenCalled();
})
test('if attachment URL, call pluralKitImport with correct arguments', async () => {
// Arrange
const args = [""];
message.content = 'pf;import';
importHelper.pluralKitImport = jest.fn().mockResolvedValue('success');
// Act
await commands.importCommand(message, args);
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith('success');
expect(importHelper.pluralKitImport).toHaveBeenCalledTimes(1);
expect(importHelper.pluralKitImport).toHaveBeenCalledWith(authorId, attachmentUrl);
})
test('if pluralKitImport returns aggregate errors with length <= 2000, send errors.', async () => {
// Arrange
const args = [""];
message.content = 'pf;import'
importHelper.pluralKitImport = jest.fn().mockRejectedValue(new AggregateError(['error1', 'error2'], 'errors'));
// Act
await commands.importCommand(message, args);
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(`errors.\n\n${enums.err.ERRORS_OCCURRED}\n\nerror1\nerror2`);
})
test('if pluralKitImport returns aggregate errors with length > 2000, call returnBufferFromText and message.reply.', async () => {
// Arrange
const args = [""];
const text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb";
const file = Buffer.from(text, 'utf-8');
const returnedBuffer = {text: 'bbbb', file: file};
const expected = {content: returnedBuffer.text, files: [{name: 'text.txt', data: returnedBuffer.file}]};
importHelper.pluralKitImport = jest.fn().mockRejectedValue(new AggregateError([text, 'error2'], 'errors'));
messageHelper.returnBufferFromText = jest.fn().mockReturnValue(returnedBuffer);
// Act
await commands.importCommand(message, args);
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(expected);
})
test('if pluralKitImport returns one error, reply with error and log it', async () => {
// Arrange
importHelper.pluralKitImport = jest.fn().mockRejectedValue(new Error('error'));
jest.spyOn(global.console, 'error').mockImplementation(() => {})
// Act
await commands.importCommand(message, args);
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith('error');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(new Error('error'));
})
})
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
})

View File

@@ -0,0 +1,103 @@
const {enums} = require('../../src/enums.js');
jest.mock('../../src/helpers/memberHelper.js', () => {
return {
memberHelper: {
addFullMember: jest.fn()
}
}
})
const {memberHelper} = require("../../src/helpers/memberHelper.js");
const {importHelper} = require('../../src/helpers/importHelper.js');
describe('importHelper', () => {
const authorId = '123';
const attachmentUrl = 'system.json';
const mockImportedMember = {
proxy_tags: [{
prefix: "SP{",
suffix: "}"
}],
display_name: "SomePerson",
avatar_url: 'oya.png',
name: 'somePerson'
}
const mockData = {
members: [mockImportedMember]
};
const mockAddReturnMember = {
proxy: "SP{text}",
displayname: "SomePerson",
propic: 'oya.png',
name: 'somePerson'
}
const mockAddReturn = {
member: mockAddReturnMember,
errors: []
}
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockData)
})
})
describe('pluralKitImport', () => {
test('if no attachment URL, throws error', async () => {
await expect(importHelper.pluralKitImport(authorId)).rejects.toThrow(enums.err.NOT_JSON_FILE);
})
test('if attachment URL, calls fetch and addFullMember and returns value', async () => {
memberHelper.addFullMember.mockResolvedValue(mockAddReturn);
const result = await importHelper.pluralKitImport(authorId, attachmentUrl);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(attachmentUrl);
expect(memberHelper.addFullMember).toHaveBeenCalledWith(authorId, mockImportedMember.name, mockImportedMember.display_name, 'SP{text}', mockImportedMember.avatar_url);
expect(result).toEqual(`Successfully added members: ${mockAddReturnMember.name}`)
})
test('if fetch fails, throws error', async () => {
global.fetch = jest.fn().mockRejectedValue("can't get");
await expect(importHelper.pluralKitImport(authorId, attachmentUrl)).rejects.toThrow(enums.err.CANNOT_FETCH_RESOURCE, "can't get file");
})
test('if json conversion fails, throws error', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.reject("not json")
})
await expect(importHelper.pluralKitImport(authorId, attachmentUrl)).rejects.toThrow(enums.err.NOT_JSON_FILE, "not json");
})
test('if addFullMember returns nothing, return correct enum', async () => {
memberHelper.addFullMember.mockResolvedValue();
const promise = importHelper.pluralKitImport(authorId, attachmentUrl);
await expect(promise).rejects.toBeInstanceOf(AggregateError);
await expect(promise).rejects.toMatchObject(AggregateError([], enums.err.NO_MEMBERS_IMPORTED));
})
test('if addFullMember throws error, catch and return error', async () => {
memberHelper.addFullMember.mockRejectedValue(new Error('error'));
await expect(importHelper.pluralKitImport(authorId, attachmentUrl)).rejects.toMatchObject(new AggregateError(['error'], enums.err.NO_MEMBERS_IMPORTED));
});
test('if addFullMember returns member but also contains error, return member and error', async () => {
// Arrange
const memberObj = {errors: ['error'], member: mockAddReturnMember};
memberHelper.addFullMember.mockResolvedValue(memberObj);
await expect(importHelper.pluralKitImport(authorId, attachmentUrl)).rejects.toMatchObject(new AggregateError(['error'], `Successfully added members: ${mockAddReturnMember.name}`));
});
});
afterEach(() => {
// restore the spy created with spyOn
jest.clearAllMocks();
});
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,16 @@
const env = require('dotenv');
env.config();
const {memberHelper} = require("../../src/helpers/memberHelper.js");
const {Message} = require("@fluxerjs/core");
const {fs} = require('fs');
const {enums} = require('../../src/enums');
const {tmp, setGracefulCleanup} = require('tmp');
jest.mock('../../src/helpers/memberHelper.js', () => {
return {memberHelper: {
getMembersByAuthor: jest.fn()
}}
return {
memberHelper: {
getMembersByAuthor: jest.fn()
}
}
})
jest.mock('tmp');
jest.mock('fs');
jest.mock('@fluxerjs/core');
const {memberHelper} = require("../../src/helpers/memberHelper.js");
const {messageHelper} = require("../../src/helpers/messageHelper.js");
describe('messageHelper', () => {
@@ -28,11 +22,11 @@ describe('messageHelper', () => {
describe('parseCommandArgs', () => {
test.each([
['pk;member', ['']],
['pk;member add somePerson "Some Person"', ['add', 'somePerson', 'Some Person']],
['pk;member add \"Some Person\"', ['add', 'Some Person']],
['pk;member add somePerson \'Some Person\'', ['add', 'somePerson', 'Some Person']],
['pk;member add somePerson \"\'Some\' Person\"', ['add', 'somePerson', 'Some Person']],
['pf;member', ['']],
['pf;member add somePerson "Some Person"', ['add', 'somePerson', 'Some Person']],
['pf;member add \"Some Person\"', ['add', 'Some Person']],
['pf;member add somePerson \'Some Person\'', ['add', 'somePerson', 'Some Person']],
['pf;member add somePerson \"\'Some\' Person\"', ['add', 'somePerson', 'Some Person']],
])('%s returns correct arguments', (content, expected) => {
// Arrange
const command = "member";
@@ -80,17 +74,16 @@ describe('messageHelper', () => {
['2', 'hello', null, undefined],
['2', '--hello', null, undefined],
['2', 'hello', attachmentUrl, undefined],
['2', '--hello', attachmentUrl,undefined],
['2', '--hello', attachmentUrl, undefined],
['3', 'hello', null, {}],
['3', '--hello', null, {}],
['3', 'hello', attachmentUrl, {}],
['3', '--hello', attachmentUrl,{}],
])('ID %s with string %s returns correct proxy', async(specificAuthorId, content, attachmentUrl, expected) => {
['3', '--hello', attachmentUrl, {}],
])('ID %s with string %s returns correct proxy', async (specificAuthorId, content, attachmentUrl, expected) => {
// Act
return messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => {
// Assert
expect(res).toEqual(expected);
})
const res = await messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl);
// Assert
expect(res).toEqual(expected);
});
})

View File

@@ -0,0 +1,67 @@
const {enums} = require("../../src/enums");
const {utils} = require("../../src/helpers/utils.js");
describe('utils', () => {
const attachmentUrl = 'oya.png';
let blob;
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
blob = new Blob([JSON.stringify({attachmentUrl: attachmentUrl})], {type: 'image/png'});
global.fetch = jest.fn(() =>
Promise.resolve({
blob: () => Promise.resolve(blob),
})
);
})
describe('checkImageFormatValidity', () => {
test('calls fetch with imageUrl and returns true if no errors', async() => {
// Act
const res = await utils.checkImageFormatValidity(attachmentUrl);
// Assert
expect(res).toBe(true);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(attachmentUrl);
})
test('throws error if fetch returns error', async() => {
// Arrange
global.fetch = jest.fn().mockRejectedValue(new Error('error'));
// Act & Assert
await expect(utils.checkImageFormatValidity(attachmentUrl)).rejects.toThrow(`${enums.err.PROPIC_CANNOT_LOAD}: error`);
})
test('throws error if blob returns error', async() => {
// Arrange
global.fetch = jest.fn(() =>
Promise.resolve({
blob: () => Promise.reject(new Error('error'))
}))
// Act & Assert
await expect(utils.checkImageFormatValidity(attachmentUrl)).rejects.toThrow('error');
})
test('throws error if blob in wrong format', async() => {
// Arrange
blob = new Blob([JSON.stringify({attachmentUrl})], {type: 'text/html'});
global.fetch = jest.fn(() =>
Promise.resolve({
blob: () => Promise.resolve(blob),
})
);
// Act & Assert
await expect(utils.checkImageFormatValidity(attachmentUrl)).rejects.toThrow(enums.err.PROPIC_FAILS_REQUIREMENTS);
})
})
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
})

View File

@@ -14,10 +14,44 @@ const {webhookHelper} = require("../../src/helpers/webhookHelper.js");
const {enums} = require("../../src/enums");
describe('webhookHelper', () => {
const channelId = '123';
const authorId = '456';
const guildId = '789';
const text = "hello";
let client, member, attachments, message, webhook;
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
client = {
channels: {
get: jest.fn().mockReturnValue(channelId)
}
}
member = {proxy: "--text", name: 'somePerson', displayname: "Some Person", propic: 'oya.png'};
attachments = {
size: 1,
first: () => {return channelId;}
};
message = {
client,
channelId: channelId,
content: text,
attachments: attachments,
author: {
id: authorId
},
guild: {
guildId: guildId
},
reply: jest.fn().mockResolvedValue(),
delete: jest.fn().mockResolvedValue()
}
webhook = {
send: async() => jest.fn().mockResolvedValue()
}
})
describe(`sendMessageAsMember`, () => {
@@ -34,9 +68,7 @@ describe('webhookHelper', () => {
author: {
id: '123'
},
guild: {
guildId: '123'
},
guildId: '123',
reply: jest.fn()
}
const member = {proxy: "--text", name: 'somePerson', displayname: "Some Person"};
@@ -50,25 +82,24 @@ describe('webhookHelper', () => {
// Arrange
messageHelper.parseProxyTags.mockResolvedValue({});
// Act
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
expect(res).toBeUndefined();
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
const res = await webhookHelper.sendMessageAsMember(client, message)
// Assert
expect(res).toBeUndefined();
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
test('calls parseProxyTags and returns if proxyMatch is undefined', async() => {
// Arrange
messageHelper.parseProxyTags.mockResolvedValue(undefined);
// Act
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
// Assert
expect(res).toBeUndefined();
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
const res = await webhookHelper.sendMessageAsMember(client, message)
// Assert
expect(res).toBeUndefined();
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
test('calls parseProxyTags with attachmentUrl', async() => {
@@ -79,27 +110,22 @@ describe('webhookHelper', () => {
return {url: 'oya.png'}
}
}
// message.attachments.set('attachment', {url: 'oya.png'})
// message.attachments.set('first', () => {return {url: 'oya.png'}})
messageHelper.parseProxyTags.mockResolvedValue(undefined);
// Act
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
// Assert
expect(res).toBeUndefined();
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, 'oya.png');
})
const res = await webhookHelper.sendMessageAsMember(client, message)
// Assert
expect(res).toBeUndefined();
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, 'oya.png');
})
test('if message matches member proxy but is not sent from a guild, throw an error', async() => {
// Arrange
message.guildId = null;
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
// Act
return webhookHelper.sendMessageAsMember(client, message).catch((res) => {
// Assert
expect(res).toEqual(new Error(enums.err.NOT_IN_SERVER));
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
// Act and Assert
await expect(webhookHelper.sendMessageAsMember(client, message)).rejects.toThrow(enums.err.NOT_IN_SERVER);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
test('if message matches member proxy and sent in a guild and has an attachment, reply to message with ping', async() => {
@@ -109,12 +135,11 @@ describe('webhookHelper', () => {
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
const expected = `${enums.misc.ATTACHMENT_SENT_BY} ${proxyMessage.member.displayname}`
// Act
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(expected);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
await webhookHelper.sendMessageAsMember(client, message)
// Assert
expect(message.reply).toHaveBeenCalledTimes(1);
expect(message.reply).toHaveBeenCalledWith(expected);
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
})
test('if message matches member proxy and sent in a guild channel and no attachment, calls replace message', async() => {
@@ -124,63 +149,26 @@ describe('webhookHelper', () => {
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
jest.spyOn(webhookHelper, 'replaceMessage').mockResolvedValue();
// Act
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
// Assert
expect(message.reply).not.toHaveBeenCalled();
expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1);
expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member);
})
await webhookHelper.sendMessageAsMember(client, message);
// Assert
expect(message.reply).not.toHaveBeenCalled();
expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1);
expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member);
})
test('if replace message throws error, throw same error', async() => {
test('if replace message throws error, throw same error and does not call message.reply', async () => {
// Arrange
message.guildId = '123';
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
jest.spyOn(webhookHelper, 'replaceMessage').mockImplementation(() => {throw new Error("error")});
jest.spyOn(webhookHelper, 'replaceMessage').mockRejectedValue(new Error("error"));
// Act
return webhookHelper.sendMessageAsMember(client, message).catch((res) => {
// Assert
expect(message.reply).not.toHaveBeenCalled();
expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1);
expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member);
expect(res).toEqual(new Error('error'));
})
await expect(webhookHelper.sendMessageAsMember(client, message)).rejects.toThrow("error");
// Assert
expect(message.reply).not.toHaveBeenCalled();
})
})
describe(`replaceMessage`, () => {
const channelId = '123';
const authorId = '456';
const guildId = '789';
const text = "hello";
const client = {
channels: {
get: jest.fn().mockReturnValue(channelId)
}
}
const member = {proxy: "--text", name: 'somePerson', displayname: "Some Person", propic: 'oya.png'};
const attachments= {
size: 1,
first: () => {return channelId;}
};
const message = {
client,
channelId: channelId,
content: text,
attachments: attachments,
author: {
id: authorId
},
guild: {
guildId: guildId
},
reply: jest.fn(),
delete: jest.fn()
}
const webhook = {
send: async() => jest.fn().mockResolvedValue()
}
test('does not call anything if text is 0 or message has no attachments', async() => {
// Arrange
@@ -192,13 +180,12 @@ describe('webhookHelper', () => {
message.attachments = noAttachments;
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
// Act
return webhookHelper.replaceMessage(client, message, emptyText, member).then(() => {
expect(webhookHelper.getOrCreateWebhook).not.toHaveBeenCalled();
expect(message.delete).not.toHaveBeenCalled();
})
await webhookHelper.replaceMessage(client, message, emptyText, member)
expect(webhookHelper.getOrCreateWebhook).not.toHaveBeenCalled();
expect(message.delete).not.toHaveBeenCalled();
})
test('calls getOrCreateWebhook and message.delete with correct arguments if text >= 0', async() => {
test('calls getOrCreateWebhook and message.delete with correct arguments if text > 0 & < 2000', async() => {
// Arrange
message.attachments = {
size: 0,
@@ -207,58 +194,108 @@ describe('webhookHelper', () => {
};
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
// Act
return webhookHelper.replaceMessage(client, message, text, member).then((res) => {
// Assert
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1);
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId);
expect(message.delete).toHaveBeenCalledTimes(1);
expect(message.delete).toHaveBeenCalledWith();
})
await webhookHelper.replaceMessage(client, message, text, member);
// Assert
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1);
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId);
expect(message.delete).toHaveBeenCalledTimes(1);
expect(message.delete).toHaveBeenCalledWith();
})
// TODO: flaky for some reason
test('calls getOrCreateWebhook and message.delete with correct arguments if attachments exist', async() => {
// TODO: Flaky for some reason. Skipping until attachments are implemented
test.skip('calls getOrCreateWebhook and message.delete with correct arguments if attachments exist', async() => {
// Arrange
const emptyText = ''
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
// Act
return webhookHelper.replaceMessage(client, message, emptyText, member).then((res) => {
// Assert
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1);
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId);
expect(message.delete).toHaveBeenCalledTimes(1);
expect(message.delete).toHaveBeenCalledWith();
})
await webhookHelper.replaceMessage(client, message, emptyText, member);
// Assert
// expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1);
// expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId);
expect(message.delete).toHaveBeenCalledTimes(1);
expect(message.delete).toHaveBeenCalledWith();
})
test('calls returnBufferFromText and console error if webhook.send returns error', async() => {
test('calls returnBufferFromText if text is more than 2000 characters', async() => {
// Arrange
const text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb";
message.content = text;
const file = Buffer.from(text, 'utf-8');
const returnedBuffer = {text: text, file: file};
const expected2ndSend = {content: returnedBuffer.text, username: member.displayname, avatar_url: member.propic, files: [{name: 'text.txt', data: returnedBuffer.file}]};
jest.mock('console', () => ({error: jest.fn()}));
const returnedBuffer = {text: 'bbbb', file: file};
const expected = {content: returnedBuffer.text, username: member.displayname, avatar_url: member.propic, files: [{name: 'text.txt', data: returnedBuffer.file}]};
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
webhook.send = jest.fn().mockImplementationOnce(async() => {throw new Error('error')});
messageHelper.returnBufferFromText = jest.fn().mockResolvedValue(returnedBuffer);
webhook.send = jest.fn();
messageHelper.returnBufferFromText = jest.fn().mockReturnValue(returnedBuffer);
// Act
return webhookHelper.replaceMessage(client, message, text, member).catch((res) => {
// Assert
expect(messageHelper.returnBufferFromText).toHaveBeenCalledTimes(1);
expect(messageHelper.returnBufferFromText).toHaveBeenCalledWith(text);
expect(webhook.send).toHaveBeenCalledTimes(2);
expect(webhook.send).toHaveBeenNthCalledWith(2, expected2ndSend);
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(new Error('error'));
})
await webhookHelper.replaceMessage(client, message, text, member);
// Assert
expect(messageHelper.returnBufferFromText).toHaveBeenCalledTimes(1);
expect(messageHelper.returnBufferFromText).toHaveBeenCalledWith(text);
expect(webhook.send).toHaveBeenCalledTimes(1);
expect(webhook.send).toHaveBeenCalledWith(expected);
})
})
describe(`getOrCreateWebhook`, () => {
let channel;
beforeEach(async () => {
channel = {
createWebhook: jest.fn().mockResolvedValue()
}
jest.spyOn(webhookHelper, 'getWebhook').mockResolvedValue(webhook);
})
test('throws error if channel does not allow webhooks', async() => {
channel.createWebhook = false;
await expect(webhookHelper.getOrCreateWebhook(client, channel)).rejects.toThrow(enums.err.NO_WEBHOOKS_ALLOWED);
})
test('calls getWebhook if channel allows webhooks and returns webhook', async() => {
const res = await webhookHelper.getOrCreateWebhook(client, channel);
expect(webhookHelper.getWebhook).toHaveBeenCalledTimes(1);
expect(webhookHelper.getWebhook).toHaveBeenCalledWith(client, channel);
expect(res).toEqual(webhook);
})
test("calls createWebhook if getWebhook doesn't return webhook", async() => {
jest.spyOn(webhookHelper, 'getWebhook').mockResolvedValue();
await webhookHelper.getOrCreateWebhook(client, channel);
expect(channel.createWebhook).toHaveBeenCalledTimes(1);
expect(channel.createWebhook).toHaveBeenCalledWith({name: 'PluralFlux Proxy Webhook'});
})
})
describe(`getWebhook`, () => {
let webhook1, webhook2, channel;
beforeEach(() => {
webhook1 = {name: 'PluralFlux Proxy Webhook'};
webhook2 = {name: 'other webhook'};
channel = {
fetchWebhooks: jest.fn().mockResolvedValue([webhook1, webhook2])
}
})
test('calls fetchWebhooks and returns correct webhook', async() => {
// Act
const res = await webhookHelper.getWebhook(client, channel);
// Assert
expect(res).toEqual(webhook1);
expect(channel.fetchWebhooks).toHaveBeenCalledTimes(1);
expect(channel.fetchWebhooks).toHaveBeenCalledWith();
})
test('if fetchWebhooks returns no webhooks, return', async() => {
// Arrange
channel.fetchWebhooks = jest.fn().mockResolvedValue([]);
// Act
const res = await webhookHelper.getWebhook(client, channel);
// Assert
expect(res).toBeUndefined();
})
})