mirror of
https://github.com/pieartsy/PluralFlux.git
synced 2026-04-15 00:35:28 +10:00
Compare commits
17 Commits
add-attach
...
webhook-re
| Author | SHA1 | Date | |
|---|---|---|---|
| 61d4e121a9 | |||
| 63b2f182bb | |||
| baf2f51773 | |||
| 6c9c253a70 | |||
| c2a88804ad | |||
| 2b31cc2ae9 | |||
| df80eca0ec | |||
| 7fead5e3d7 | |||
| 428310dfad | |||
| dc0de4b092 | |||
| 6898e3142c | |||
| adcb05a38b | |||
|
|
db3c588745 | ||
|
|
56d7f7d1fa | ||
|
|
8f6ae668b0 | ||
|
|
211a705f31 | ||
|
|
69c242350f |
@@ -1,5 +1,5 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
.env.jest
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
|
||||
2
.env.jest
Normal file
2
.env.jest
Normal file
@@ -0,0 +1,2 @@
|
||||
FLUXER_BOT_TOKEN=jest-fluxer-bot-token
|
||||
POSTGRES_PASSWORD=jest-postgres-password
|
||||
49
.gitea/workflows/build-dev.yml
Normal file
49
.gitea/workflows/build-dev.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Build Dev instance
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "Develop"]
|
||||
pull_request:
|
||||
branches: ["develop", "Develop"]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: login to gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ gitea.server_url }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.GITEA }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
engineering.sanya.gay/pluralflux/pluralflux-dev:latest
|
||||
|
||||
- name: Deploy bot
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
port: 22
|
||||
script: |
|
||||
cd ${{ secrets.BOT_DIRECTORY }}
|
||||
docker compose pull
|
||||
docker compose up -d pluralflux-dev
|
||||
49
.gitea/workflows/build-main.yml
Normal file
49
.gitea/workflows/build-main.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
name: nodeJS remote worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: login to gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ gitea.server_url }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.GITEA }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
engineering.sanya.gay/pluralflux/pluralflux:latest
|
||||
|
||||
- name: Deploy bot
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
port: 22
|
||||
script: |
|
||||
cd ${{ secrets.BOT_DIRECTORY }}
|
||||
docker compose pull
|
||||
docker compose up -d pluralflux-prod
|
||||
26
.gitea/workflows/sync-from-mirror.yaml
Normal file
26
.gitea/workflows/sync-from-mirror.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Auto-Sync from Mirror
|
||||
on:
|
||||
push:
|
||||
repository: "Pluralflux/Pluralflux"
|
||||
branches: [main,develop]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Fork
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Pull from Mirror
|
||||
run: |
|
||||
git remote add upstream https://engineering.sanya.gay/PluralFlux/PluralFlux.git
|
||||
git fetch upstream --prune
|
||||
git reset --hard origin/main
|
||||
git push origin "refs/remotes/upstream/*:refs/heads/*" --force-with-lease
|
||||
git merge upstream/main -m "Syncing from github"
|
||||
git push origin main
|
||||
39
.github/workflows/node.js.yml
vendored
39
.github/workflows/node.js.yml
vendored
@@ -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
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,8 +1,13 @@
|
||||
node_modules
|
||||
node_modules/
|
||||
build/
|
||||
tmp/
|
||||
temp/
|
||||
.idea
|
||||
secrets/
|
||||
config.json
|
||||
coverage
|
||||
config.json
|
||||
log.txt
|
||||
.env
|
||||
oya.png
|
||||
oya.png
|
||||
variables.env
|
||||
.env.production
|
||||
32
CONTRIBUTING.md
Normal file
32
CONTRIBUTING.md
Normal 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.
|
||||
@@ -7,4 +7,4 @@ FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
CMD ["node", "src/bot.js"]
|
||||
CMD ["npm", "start"]
|
||||
60
LICENSE
Normal file
60
LICENSE
Normal 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.
|
||||
|
||||
15
README.md
15
README.md
@@ -7,8 +7,6 @@ PluralFlux is a proxybot akin to PluralKit and Tupperbox, but for [Fluxer](https
|
||||
|
||||
[Sponsor the project](https://github.com/sponsors/pieartsy)
|
||||
|
||||
If it's not running at the moment, it's because my computer crashed or something. I'm looking to move running it to a somewhat more permanent solution.
|
||||
|
||||
## Commands
|
||||
All commands are prefixed by `pf;`. Currently only a few are implemented.
|
||||
|
||||
@@ -36,13 +34,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.
|
||||
|
||||
36
compose.yaml
36
compose.yaml
@@ -1,46 +1,26 @@
|
||||
services:
|
||||
main:
|
||||
build: .
|
||||
image: engineering.sanya.gay/pluralflux/pluralflux
|
||||
container_name: pluralflux
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- pluralflux-net
|
||||
env_file: "variables.env"
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
container_name: pluralflux-postgres
|
||||
environment:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pwd
|
||||
secrets:
|
||||
- postgres_pwd
|
||||
env_file: "variables.env"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql
|
||||
- ./pgBackup:/mnt/pgBackup
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- pluralflux-net
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: pluralflux-pgadmin
|
||||
ports:
|
||||
- "5050:80"
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: code@asterfialla.com
|
||||
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/postgres_pwd
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||
secrets:
|
||||
- postgres_pwd
|
||||
env_file: "variables.env"
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- pluralflux-net
|
||||
|
||||
networks:
|
||||
pluralflux-net:
|
||||
|
||||
volumes:
|
||||
- pgadmindata:/var/lib/pgadmin
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
secrets:
|
||||
postgres_pwd:
|
||||
file: ./secrets/postgres-password.txt
|
||||
pgadmindata:
|
||||
26
database/data-source.ts
Normal file
26
database/data-source.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import "reflect-metadata"
|
||||
import { DataSource } from "typeorm"
|
||||
import * as env from 'dotenv';
|
||||
import * as path from "path";
|
||||
|
||||
env.config();
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: "postgres",
|
||||
host: process.env.POSTGRES_ENDPOINT,
|
||||
port: 5432,
|
||||
username: "postgres",
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: "postgres",
|
||||
synchronize: false,
|
||||
logging: false,
|
||||
entities: [path.join(__dirname, "./entity/*.{ts,js}")],
|
||||
migrations: [path.join(__dirname, "./migrations/*.{ts,js}")],
|
||||
migrationsRun: true,
|
||||
migrationsTableName: 'migrations',
|
||||
migrationsTransactionMode: 'all',
|
||||
invalidWhereValuesBehavior: {
|
||||
null: "sql-null",
|
||||
undefined: "throw",
|
||||
},
|
||||
});
|
||||
40
database/entity/Member.ts
Normal file
40
database/entity/Member.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Unique} from "typeorm"
|
||||
|
||||
@Entity({name: "Member", synchronize: true})
|
||||
@Unique("UQ_Member_userid_name", ['userid', 'name'])
|
||||
export class Member {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
userid: string
|
||||
|
||||
@Column({
|
||||
length: 100
|
||||
})
|
||||
name: string
|
||||
|
||||
@Column({
|
||||
type: "varchar",
|
||||
nullable: true,
|
||||
length: 100
|
||||
})
|
||||
displayname: string
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
})
|
||||
proxy: string
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
})
|
||||
propic: string
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date
|
||||
}
|
||||
14
database/migrations/1772417745487-update.ts
Normal file
14
database/migrations/1772417745487-update.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Update1772417745487 implements MigrationInterface {
|
||||
name = 'Update1772417745487'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "Member" ("id" SERIAL NOT NULL, "userid" character varying NOT NULL, "name" character varying(100) NOT NULL, "displayname" character varying(100), "proxy" character varying, "propic" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_235428a1d87c5f639ef7b7cf170" PRIMARY KEY ("id"))`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "Member"`);
|
||||
}
|
||||
|
||||
}
|
||||
12
database/migrations/1772419448503-add-data.ts
Normal file
12
database/migrations/1772419448503-add-data.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddData1772419448503 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`INSERT INTO "Member"(id, userid, name,displayname, proxy, propic, "createdAt", "updatedAt") SELECT id,userid, name,displayname, proxy, propic, "createdAt", "updatedAt" FROM "Members";`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`TRUNCATE TABLE "Member"`);
|
||||
}
|
||||
}
|
||||
17
database/migrations/1772825438973-delete-duplicates.ts
Normal file
17
database/migrations/1772825438973-delete-duplicates.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DeleteDuplicates1772825438973 implements MigrationInterface {
|
||||
name= "DeleteDuplicates1772825438973"
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DELETE
|
||||
FROM "Member" a USING "Member" b
|
||||
WHERE a.id
|
||||
> b.id
|
||||
AND a.name = b.name
|
||||
AND a.userid = b.userid;`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
14
database/migrations/1772830252670-update.ts
Normal file
14
database/migrations/1772830252670-update.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Update1772830252670 implements MigrationInterface {
|
||||
name = 'Update1772830252670'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Member" ADD CONSTRAINT "UQ_Member_userid_name" UNIQUE ("userid", "name")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Member" DROP CONSTRAINT "UQ_Member_userid_name"`);
|
||||
}
|
||||
|
||||
}
|
||||
2198
package-lock.json
generated
2198
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -7,24 +7,36 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/pieartsy/PluralFlux.git"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fluxerjs/core": "^1.1.5",
|
||||
"@fluxerjs/core": "^1.2.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"pg": "^8.18.0",
|
||||
"pg": "^8.19.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"pm2": "^6.0.14",
|
||||
"sequelize": "^6.37.7",
|
||||
"tmp": "^0.2.5"
|
||||
"psql": "^0.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tmp": "^0.2.5",
|
||||
"typeorm": "^0.3.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@fetch-mock/jest": "^0.2.20",
|
||||
"@types/node": "^25.3.3",
|
||||
"babel-jest": "^30.2.0",
|
||||
"jest": "^30.2.0"
|
||||
"fetch-mock": "^12.6.0",
|
||||
"jest": "^30.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"start": "ts-node src/bot.js",
|
||||
"new-migration": "typeorm-ts-node-commonjs migration:create database/migrations/update",
|
||||
"generate-db": "typeorm-ts-node-commonjs migration:generate -d database/data-source.ts database/migrations/update",
|
||||
"run-migration": "typeorm-ts-node-commonjs migration:run -d database/data-source.ts"
|
||||
}
|
||||
}
|
||||
|
||||
95
src/bot.js
95
src/bot.js
@@ -1,87 +1,108 @@
|
||||
import { Client, Events } 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';
|
||||
const {Client, Events, Message} = require('@fluxerjs/core');
|
||||
const {messageHelper} = require("./helpers/messageHelper.js");
|
||||
const {enums} = require("./enums.js");
|
||||
const {commands} = require("./commands.js");
|
||||
const {webhookHelper} = require("./helpers/webhookHelper.js");
|
||||
const env = require('dotenv');
|
||||
const {utils} = require("./helpers/utils.js");
|
||||
const { AppDataSource } = require("../database/data-source");
|
||||
|
||||
env.config();
|
||||
|
||||
const token = process.env.FLUXER_BOT_TOKEN;
|
||||
const debug = process.env.debug;
|
||||
|
||||
if (!token) {
|
||||
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new Client({ intents: 0 });
|
||||
client = new Client({ intents: 0 });
|
||||
|
||||
module.exports.client = client;
|
||||
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
try {
|
||||
// Ignore bots and messages without content
|
||||
if (message.author.bot || !message.content) return;
|
||||
await module.exports.handleMessageCreate(message);
|
||||
});
|
||||
|
||||
/**
|
||||
* Calls functions based off the contents of a message object.
|
||||
*
|
||||
* @async
|
||||
* @param {Message} message - The message object
|
||||
*
|
||||
**/
|
||||
module.exports.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);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
// return await message.reply(error.message);
|
||||
if(debug){console.error("An error occurred at unix timestamp " + Date.now() + "while processing the command: " + message + " with error:" + error);}
|
||||
else{console.error(error);}
|
||||
process.exit(2); //need this for now just to make sure the bot continues to restart on errors, since it would seem that fluxer.js doesn't define custom error types. TODO: map out some exit codes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.on(Events.Ready, () => {
|
||||
console.log(`Logged in as ${client.user?.username}`);
|
||||
if(debug){console.log(Date.now() + `: Currently running in debug mode!`)}
|
||||
});
|
||||
|
||||
let guildCount = 0;
|
||||
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);
|
||||
};
|
||||
module.exports.login = async function() {
|
||||
try {
|
||||
if (!AppDataSource.isInitialized) {
|
||||
await AppDataSource.initialize();
|
||||
}
|
||||
await client.login(token);
|
||||
} 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()
|
||||
{
|
||||
exports.login();
|
||||
}
|
||||
|
||||
main();
|
||||
148
src/commands.js
148
src/commands.js
@@ -1,36 +1,62 @@
|
||||
import {messageHelper} from "./helpers/messageHelper.js";
|
||||
import {enums} from "./enums.js";
|
||||
import {memberHelper} from "./helpers/memberHelper.js";
|
||||
import {EmbedBuilder} from "@fluxerjs/core";
|
||||
import {importHelper} from "./helpers/importHelper.js";
|
||||
const {messageHelper} = require("./helpers/messageHelper.js");
|
||||
const {enums} = require("./enums.js");
|
||||
const {memberHelper} = require("./helpers/memberHelper.js");
|
||||
const {EmbedBuilder} = require("@fluxerjs/core");
|
||||
const {importHelper} = require("./helpers/importHelper.js");
|
||||
|
||||
const cmds = new Map();
|
||||
const commands = {
|
||||
commandsMap: new Map(),
|
||||
aliasesMap: new Map()
|
||||
};
|
||||
|
||||
cmds.set('member', {
|
||||
commands.aliasesMap.set('m', {command: 'member'})
|
||||
|
||||
commands.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 commands.memberCommand(message, args)
|
||||
}
|
||||
})
|
||||
|
||||
cmds.set('help', {
|
||||
/**
|
||||
* Calls the member-related functions.
|
||||
*
|
||||
* @async
|
||||
* @param {Message} message - The message object
|
||||
* @param {string[]} args - The parsed arguments
|
||||
*
|
||||
**/
|
||||
commands.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]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
commands.commandsMap.set('help', {
|
||||
description: enums.help.SHORT_DESC_HELP,
|
||||
async execute(message) {
|
||||
const fields = [...cmds.entries()].map(([name, cmd]) => ({
|
||||
const fields = [...commands.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', {
|
||||
commands.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 commands.importCommand(message, args);
|
||||
}
|
||||
})
|
||||
|
||||
export const commands = cmds;
|
||||
/**
|
||||
* Calls the import-related functions.
|
||||
*
|
||||
* @async
|
||||
* @param {Message} message - The message object
|
||||
* @param {string[]} args - The parsed arguments
|
||||
*
|
||||
**/
|
||||
commands.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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.commands = commands;
|
||||
@@ -1,84 +0,0 @@
|
||||
import {DataTypes, Sequelize} from 'sequelize';
|
||||
import * as env from 'dotenv';
|
||||
|
||||
env.config();
|
||||
|
||||
const password = process.env.POSTGRES_PASSWORD;
|
||||
|
||||
if (!password) {
|
||||
console.error("Missing POSTGRES_PWD environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = {};
|
||||
|
||||
const sequelize = new Sequelize('postgres', 'postgres', password, {
|
||||
host: 'localhost',
|
||||
logging: false,
|
||||
dialect: 'postgres'
|
||||
});
|
||||
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
db.members = sequelize.define('Member', {
|
||||
userid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
displayname: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
propic: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
proxy: {
|
||||
type: DataTypes.STRING,
|
||||
}
|
||||
});
|
||||
|
||||
db.systems = sequelize.define('System', {
|
||||
userid: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
fronter: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
grouptag: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
autoproxy: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks Sequelize database connection.
|
||||
*/
|
||||
db.check_connection = async function() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs Sequelize models.
|
||||
*/
|
||||
async function syncModels() {
|
||||
await sequelize.sync().then(() => {
|
||||
console.log('Models synced successfully.');
|
||||
}).catch((err) => {
|
||||
console.error('Syncing models did not work', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export const database = db;
|
||||
35
src/enums.js
35
src/enums.js
@@ -1,17 +1,18 @@
|
||||
const helperEnums = {};
|
||||
const enums = {};
|
||||
|
||||
helperEnums.err = {
|
||||
enums.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,30 +20,34 @@ 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 = {
|
||||
enums.help = {
|
||||
SHORT_DESC_HELP: "Lists available commands.",
|
||||
SHORT_DESC_MEMBER: "Accesses subcommands related to proxy members.",
|
||||
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 = {
|
||||
ATTACHMENT_SENT_BY: "Attachment sent by:"
|
||||
enums.misc = {
|
||||
ATTACHMENT_SENT_BY: "Attachment sent by:",
|
||||
ATTACHMENT_EXPIRATION_WARNING: "**NOTE:** Because this profile picture is hosted on Fluxer, it will expire. To avoid this, upload the picture to another website like <https://imgbb.com/> and link to it directly.",
|
||||
FLUXER_ATTACHMENT_URL: "https://fluxerusercontent.com/attachments/"
|
||||
|
||||
}
|
||||
|
||||
export const enums = helperEnums;
|
||||
module.exports.enums = enums;
|
||||
@@ -1,43 +1,58 @@
|
||||
import {enums} from "../enums.js";
|
||||
import {memberHelper} from "./memberHelper.js";
|
||||
const {enums} = require("../enums.js");
|
||||
const {memberHelper} = require("./memberHelper.js");
|
||||
|
||||
const ih = {};
|
||||
const importHelper = {};
|
||||
|
||||
/**
|
||||
* Tries to import from Pluralkit.
|
||||
*
|
||||
* @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) {
|
||||
importHelper.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;
|
||||
exports.importHelper = importHelper;
|
||||
@@ -1,44 +1,149 @@
|
||||
import {database} from '../database.js';
|
||||
import {enums} from "../enums.js";
|
||||
import {EmptyResultError, Op} from "sequelize";
|
||||
import {EmbedBuilder} from "@fluxerjs/core";
|
||||
const {enums} = require("../enums.js");
|
||||
const {EmbedBuilder} = require("@fluxerjs/core");
|
||||
const {utils} = require("./utils.js");
|
||||
const {memberRepo} = require("../repositories/memberRepo.js");
|
||||
|
||||
const mh = {};
|
||||
const memberHelper = {};
|
||||
|
||||
// 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;
|
||||
memberHelper.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) {
|
||||
let memberName, command, isHelp = false;
|
||||
// checks whether command is in list, otherwise assumes it's a name
|
||||
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 memberHelper.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 memberHelper.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.
|
||||
* @returns {Promise<string>} - A help message
|
||||
* @throws {Error} When there's no member or a command is not recognized.
|
||||
*/
|
||||
memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) {
|
||||
if (!command && !memberName && !isHelp) {
|
||||
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||
}
|
||||
else if (isHelp) {
|
||||
return memberHelper.sendHelpEnum(command);
|
||||
}
|
||||
else if (command === "list") {
|
||||
return await memberHelper.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 memberHelper.memberCommandHandler(authorId, command, memberName, values, attachmentUrl, attachmentExpiration);
|
||||
}
|
||||
else if (memberName && values.length === 0) {
|
||||
return await memberHelper.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
|
||||
*/
|
||||
memberHelper.sendCurrentValue = async function(authorId, memberName, command= null) {
|
||||
const member = await memberRepo.getMemberByName(authorId, memberName);
|
||||
if (!member) throw new Error(enums.err.NO_MEMBER);
|
||||
|
||||
if (!command) {
|
||||
return memberHelper.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.
|
||||
*/
|
||||
memberHelper.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,38 @@ 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> | Promise <EmbedBuilder> | Promise<{EmbedBuilder, [string], string}>}
|
||||
*/
|
||||
memberHelper.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
|
||||
switch (command) {
|
||||
case 'new':
|
||||
return await memberHelper.addNewMember(authorId, memberName, values, attachmentUrl, attachmentExpiration);
|
||||
case 'remove':
|
||||
return await memberHelper.removeMember(authorId, memberName);
|
||||
case 'name':
|
||||
return await mh.updateName(authorId, args).catch((e) => {
|
||||
throw e
|
||||
});
|
||||
return await memberHelper.updateName(authorId, memberName, values[0]);
|
||||
case 'displayname':
|
||||
return await mh.updateDisplayName(authorId, args).catch((e) => {
|
||||
throw e
|
||||
});
|
||||
return await memberHelper.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 memberHelper.updateProxy(authorId, memberName, values[0]);
|
||||
case 'propic':
|
||||
return await mh.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) => {
|
||||
throw e
|
||||
});
|
||||
return await memberHelper.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration);
|
||||
default:
|
||||
return member;
|
||||
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,26 +193,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;
|
||||
memberHelper.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 memberHelper.addFullMember(authorId, memberName, displayName, proxy, propic, attachmentExpiration);
|
||||
const memberInfoEmbed = memberHelper.getMemberInfo(memberObj.member);
|
||||
return {embed: memberInfoEmbed, errors: memberObj.errors, success: `${memberName} has been added successfully.`}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,26 +214,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!`;
|
||||
}
|
||||
memberHelper.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 memberHelper.updateMemberField(authorId, memberName, "name", trimmedName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,36 +232,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;
|
||||
}
|
||||
memberHelper.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 memberHelper.updateMemberField(authorId, membername, "displayname", trimmedName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,24 +254,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
|
||||
});
|
||||
}
|
||||
memberHelper.updateProxy = async function (authorId, memberName, proxy) {
|
||||
// Throws error if exists
|
||||
await memberHelper.checkIfProxyExists(authorId, proxy);
|
||||
|
||||
return await memberHelper.updateMemberField(authorId, memberName, "proxy", proxy);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,53 +270,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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`);
|
||||
});
|
||||
memberHelper.updatePropic = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
|
||||
const imgUrl = values ?? attachmentUrl;
|
||||
// Throws error if invalid
|
||||
await utils.checkImageFormatValidity(imgUrl);
|
||||
const expirationWarning = utils.setExpirationWarning(imgUrl, attachmentExpiration);
|
||||
return await memberHelper.updateMemberField(authorId, memberName, "propic", imgUrl, expirationWarning);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,27 +289,17 @@ 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;
|
||||
memberHelper.removeMember = async function (authorId, memberName) {
|
||||
const destroyed = await memberRepo.removeMember(authorId, memberName);
|
||||
if (destroyed > 0) {
|
||||
return `Member "${memberName}" has been deleted.`;
|
||||
} else {
|
||||
throw new Error(`${enums.err.NO_MEMBER}`);
|
||||
}
|
||||
|
||||
const memberName = args[1];
|
||||
return 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}`);
|
||||
})
|
||||
}
|
||||
|
||||
/*======Non-Subcommands======*/
|
||||
@@ -288,24 +310,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<{Members, 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}`);
|
||||
}
|
||||
});
|
||||
memberHelper.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) {
|
||||
const existingMember = await memberRepo.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,201 +347,75 @@ 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 memberHelper.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;
|
||||
let isValidPropic, expirationWarning;
|
||||
if (propic && propic.length > 0) {
|
||||
await mh.checkImageFormatValidity(propic).then(() => {
|
||||
isValidPropic = true;
|
||||
}).catch((e) => {
|
||||
try {
|
||||
isValidPropic = await utils.checkImageFormatValidity(propic);
|
||||
expirationWarning = utils.setExpirationWarning(propic, attachmentExpiration);
|
||||
if (expirationWarning) {
|
||||
errors.push(expirationWarning);
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
errors.push(`Tried to set profile picture to \"${propic}\". ${e.message}. ${enums.err.SET_TO_NULL}`);
|
||||
isValidPropic = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
const member = await database.members.create({
|
||||
|
||||
const member = await memberRepo.createMember({
|
||||
name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
|
||||
});
|
||||
|
||||
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} [expirationWarning] - The attachment expiration warning (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];
|
||||
let fluxerPropicWarning;
|
||||
|
||||
// indicates that an attachment was uploaded on Fluxer directly
|
||||
if (columnName === "propic" && args[3]) {
|
||||
fluxerPropicWarning = mh.setExpirationWarning(args[3]);
|
||||
}
|
||||
return 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 ?? ''}.`;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the warning for an expiration date.
|
||||
*
|
||||
* @param {string} expirationString - An expiration date string.
|
||||
* @returns {string} A description of the expiration, interpolating the expiration string.
|
||||
*/
|
||||
mh.setExpirationWarning = function (expirationString) {
|
||||
let expirationDate = new Date(expirationString);
|
||||
if (!isNaN(expirationDate.valueOf())) {
|
||||
expirationDate = expirationDate.toDateString();
|
||||
return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like <https://imgbb.com/> and link to it directly`
|
||||
memberHelper.updateMemberField = async function (authorId, memberName, columnName, value, expirationWarning = null) {
|
||||
const res = await memberRepo.updateMemberField(authorId, memberName, columnName, value);
|
||||
if (res === 0) {
|
||||
throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
|
||||
} else {
|
||||
return `Updated ${columnName} for ${memberName} to ${value}${expirationWarning ? `. ${expirationWarning}.` : '.'}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {{Member, string[]}} 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);
|
||||
}
|
||||
});
|
||||
memberHelper.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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,72 +427,17 @@ mh.getMemberInfo = async function (authorId, memberName) {
|
||||
* @returns {Promise<EmbedBuilder>} The info for all members.
|
||||
* @throws {Error} When there are no members for an author.
|
||||
*/
|
||||
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]) => ({
|
||||
memberHelper.getAllMembersInfo = async function (authorId, authorName) {
|
||||
const members = await memberRepo.getMembersByAuthor(authorId);
|
||||
if (members.length === 0) throw Error(enums.err.USER_NO_MEMBERS);
|
||||
const fields = [...members.entries()].map(([index, member]) => ({
|
||||
name: member.name, value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, inline: true,
|
||||
}));
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member based on the author and proxy tag.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message.
|
||||
* @param {string} memberName - The member's name.
|
||||
* @returns {Promise<model>} The member object.
|
||||
* @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.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @returns {Promise<model[] | null>} The member object array.
|
||||
*/
|
||||
mh.getMembersByAuthor = async function (authorId) {
|
||||
return await database.members.findAll({where: {userid: authorId}});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if proxy exists for a member.
|
||||
*
|
||||
@@ -596,23 +446,39 @@ mh.getMembersByAuthor = async function (authorId) {
|
||||
* @returns {Promise<boolean> } Whether the proxy exists.
|
||||
* @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);
|
||||
memberHelper.checkIfProxyExists = async function (authorId, proxy) {
|
||||
const splitProxy = proxy.trim().split("text");
|
||||
if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY);
|
||||
if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER);
|
||||
|
||||
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 memberRepo.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.
|
||||
*/
|
||||
memberHelper.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);
|
||||
}
|
||||
|
||||
|
||||
export const memberHelper = mh;
|
||||
module.exports.memberHelper = memberHelper;
|
||||
@@ -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 {memberRepo} = require('../repositories/memberRepo.js');
|
||||
|
||||
const msgh = {};
|
||||
|
||||
msgh.prefix = "pf;"
|
||||
|
||||
setGracefulCleanup();
|
||||
|
||||
/**
|
||||
* Parses and slices up message arguments, retaining quoted strings.
|
||||
*
|
||||
@@ -38,14 +32,14 @@ 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);
|
||||
const members = await memberRepo.getMembersByAuthor(authorId);
|
||||
// If an author has no members, no sense in searching for proxy
|
||||
if (members.length === 0) {
|
||||
return;
|
||||
@@ -86,4 +80,4 @@ msgh.returnBufferFromText = function (text) {
|
||||
return {text: text, file: undefined}
|
||||
}
|
||||
|
||||
export const messageHelper = msgh;
|
||||
module.exports.messageHelper = msgh;
|
||||
|
||||
57
src/helpers/utils.js
Normal file
57
src/helpers/utils.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const {enums} = require('../enums');
|
||||
|
||||
const utils = {};
|
||||
|
||||
utils.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.
|
||||
*/
|
||||
utils.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the warning that a Fluxer-uploaded image will expire.
|
||||
*
|
||||
* @param {string | null} [imgUrl] - An image URL.
|
||||
* @param {string | null} [expirationString] - An expiration date string.
|
||||
* @returns {string | null} A description of the expiration, or null.
|
||||
*/
|
||||
utils.setExpirationWarning = function (imgUrl = null, expirationString = null) {
|
||||
if (imgUrl && imgUrl.startsWith(enums.misc.FLUXER_ATTACHMENT_URL)) {
|
||||
return enums.misc.ATTACHMENT_EXPIRATION_WARNING;
|
||||
}
|
||||
else if (expirationString) {
|
||||
let expirationDate = new Date(expirationString);
|
||||
if (!isNaN(expirationDate.valueOf())) {
|
||||
return `${enums.misc.ATTACHMENT_EXPIRATION_WARNING}. Expiration date: *${expirationString}*.`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports.utils = utils;
|
||||
@@ -1,8 +1,8 @@
|
||||
import {messageHelper} from "./messageHelper.js";
|
||||
import {Webhook, Channel, Message, Client} from '@fluxerjs/core';
|
||||
import {enums} from "../enums.js";
|
||||
const {messageHelper} = require("./messageHelper.js");
|
||||
const {Webhook, Channel, Message, Client} = require('@fluxerjs/core');
|
||||
const {enums} = require("../enums.js");
|
||||
|
||||
const wh = {};
|
||||
const webhookHelper = {};
|
||||
|
||||
const name = 'PluralFlux Proxy Webhook';
|
||||
|
||||
@@ -13,9 +13,9 @@ const name = 'PluralFlux Proxy Webhook';
|
||||
* @param {Message} message - The full message object.
|
||||
* @throws {Error} When the proxy message is not in a server.
|
||||
*/
|
||||
wh.sendMessageAsMember = async function(client, message) {
|
||||
webhookHelper.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 webhookHelper.replaceMessage(client, message, proxyMatch.message, proxyMatch.member);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,24 +39,23 @@ wh.sendMessageAsMember = async function(client, message) {
|
||||
* @param {model} member - A member object from the database.
|
||||
* @throws {Error} When there's no message to send.
|
||||
*/
|
||||
wh.replaceMessage = async function(client, message, text, member) {
|
||||
webhookHelper.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 webhookHelper.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();
|
||||
}
|
||||
}
|
||||
@@ -69,10 +68,10 @@ wh.replaceMessage = async function(client, message, text, member) {
|
||||
* @returns {Webhook} A webhook object.
|
||||
* @throws {Error} When no webhooks are allowed in the channel.
|
||||
*/
|
||||
wh.getOrCreateWebhook = async function(client, channel) {
|
||||
webhookHelper.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 webhookHelper.getWebhook(client, channel)
|
||||
if (!webhook) {
|
||||
webhook = await channel.createWebhook({name: name});
|
||||
}
|
||||
@@ -86,18 +85,12 @@ wh.getOrCreateWebhook = async function(client, channel) {
|
||||
* @param {Channel} channel - The channel the message was sent in.
|
||||
* @returns {Webhook} A webhook object.
|
||||
*/
|
||||
wh.getWebhook = async function(client, channel) {
|
||||
webhookHelper.getWebhook = async function(client, channel) {
|
||||
const channelWebhooks = await channel?.fetchWebhooks() ?? [];
|
||||
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;
|
||||
module.exports.webhookHelper = webhookHelper;
|
||||
74
src/repositories/memberRepo.js
Normal file
74
src/repositories/memberRepo.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const Member = require("../../database/entity/Member");
|
||||
const { AppDataSource } = require("../../database/data-source");
|
||||
const {ILike} = require("typeorm");
|
||||
const members = AppDataSource.getRepository(Member.Member)
|
||||
|
||||
const memberRepo = {};
|
||||
/**
|
||||
* Gets a member based on the author and proxy tag.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message.
|
||||
* @param {string} memberName - The member's name.
|
||||
* @returns {Promise<Member | null>} The member object or null if not found.
|
||||
*/
|
||||
memberRepo.getMemberByName = async function (authorId, memberName) {
|
||||
return await members.findOne({where: {userid: authorId, name: ILike(memberName)}});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all members belonging to the author.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @returns {Promise<Member[]>} The member object array.
|
||||
*/
|
||||
memberRepo.getMembersByAuthor = async function (authorId) {
|
||||
return await members.findBy({userid: authorId});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a member.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string} memberName - The name of the member to remove
|
||||
* @returns {Promise<number>} Number of results removed.
|
||||
*/
|
||||
memberRepo.removeMember = async function (authorId, memberName) {
|
||||
const deleted = await members.delete({ name: ILike(memberName), userid: authorId })
|
||||
return deleted.affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a member with full details.
|
||||
*
|
||||
* @async
|
||||
* @param {{name: string, userid: string, displayname: (string|null), proxy: (string|null), propic: (string|null)}} createObj - Object with parameters in it
|
||||
* @returns {Promise<Member>} A successful inserted object.
|
||||
*/
|
||||
memberRepo.createMember = async function (createObj) {
|
||||
return await members.save({
|
||||
name: createObj.name, userid: createObj.userid, displayname: createObj.displayname, proxy: createObj.proxy, propic: createObj.propic
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates one fields for a member in the database.
|
||||
*
|
||||
* @async
|
||||
* @param {string} authorId - The author of the message
|
||||
* @param {string} memberName - The member to update
|
||||
* @param {string} columnName - The column name to update.
|
||||
* @param {string} value - The value to update to.
|
||||
* @returns {Promise<number>} A successful update.
|
||||
*/
|
||||
memberRepo.updateMemberField = async function (authorId, memberName, columnName, value) {
|
||||
const updated = await members.update({
|
||||
name: ILike(memberName),
|
||||
userid: authorId
|
||||
}, {[columnName]: value})
|
||||
return updated.affected;
|
||||
}
|
||||
|
||||
module.exports.memberRepo = memberRepo;
|
||||
300
tests/bot.test.js
Normal file
300
tests/bot.test.js
Normal file
@@ -0,0 +1,300 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../database/data-source.ts', () => {
|
||||
return {
|
||||
AppDataSource: {
|
||||
isInitialized: false,
|
||||
initialize: jest.fn().mockResolvedValue()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const {Client, Events} = require('@fluxerjs/core');
|
||||
const {messageHelper} = require("../src/helpers/messageHelper.js");
|
||||
|
||||
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
210
tests/commands.test.js
Normal 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();
|
||||
});
|
||||
})
|
||||
103
tests/helpers/importHelper.test.js
Normal file
103
tests/helpers/importHelper.test.js
Normal 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
@@ -1,23 +1,17 @@
|
||||
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()
|
||||
}}
|
||||
jest.mock('../../src/repositories/memberRepo.js', () => {
|
||||
return {
|
||||
memberRepo: {
|
||||
getMembersByAuthor: jest.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('tmp');
|
||||
jest.mock('fs');
|
||||
jest.mock('@fluxerjs/core');
|
||||
|
||||
const {messageHelper} = require("../../src/helpers/messageHelper.js");
|
||||
const {memberRepo} = require("../../src/repositories/memberRepo");
|
||||
|
||||
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";
|
||||
@@ -60,7 +54,7 @@ describe('messageHelper', () => {
|
||||
const attachmentUrl = "../oya.png"
|
||||
|
||||
beforeEach(() => {
|
||||
memberHelper.getMembersByAuthor = jest.fn().mockImplementation((specificAuthorId) => {
|
||||
memberRepo.getMembersByAuthor = jest.fn().mockImplementation((specificAuthorId) => {
|
||||
if (specificAuthorId === "1") return membersFor1;
|
||||
if (specificAuthorId === "2") return membersFor2;
|
||||
if (specificAuthorId === "3") return membersFor3;
|
||||
@@ -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);
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
90
tests/helpers/utils.test.js
Normal file
90
tests/helpers/utils.test.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const {enums} = require("../../src/enums");
|
||||
|
||||
const {utils} = require("../../src/helpers/utils.js");
|
||||
|
||||
describe('utils', () => {
|
||||
|
||||
const attachmentUrl = 'oya.png';
|
||||
const expirationString = new Date("2026-01-01").toDateString();
|
||||
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);
|
||||
})
|
||||
})
|
||||
|
||||
describe('setExpirationWarning', () => {
|
||||
test('sets warning if image Url starts with Fluxer host', () => {
|
||||
// Act
|
||||
const result = utils.setExpirationWarning(`${enums.misc.FLUXER_ATTACHMENT_URL}${attachmentUrl}`);
|
||||
// Assert
|
||||
expect(result).toEqual(enums.misc.ATTACHMENT_EXPIRATION_WARNING);
|
||||
})
|
||||
|
||||
test('sets warning if expiration string exists', () => {
|
||||
const result = utils.setExpirationWarning(null, expirationString);
|
||||
// Assert
|
||||
expect(result).toEqual(`${enums.misc.ATTACHMENT_EXPIRATION_WARNING}. Expiration date: *${expirationString}*.`);
|
||||
})
|
||||
|
||||
test('returns null if img url does not start iwth fluxer host and no expiration', () => {
|
||||
// Act
|
||||
const result = utils.setExpirationWarning(attachmentUrl);
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
})
|
||||
@@ -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();
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2021"
|
||||
],
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./database/build",
|
||||
"rootDir": "./database",
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
7
variables.env
Normal file
7
variables.env
Normal file
@@ -0,0 +1,7 @@
|
||||
FLUXER_BOT_TOKEN=<>
|
||||
POSTGRES_PASSWORD=<>
|
||||
POSTGRES_ENDPOINT=postgres
|
||||
PGADMIN_DEFAULT_EMAIL: <>
|
||||
PGADMIN_DEFAULT_PASSWORD: <>
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||
Reference in New Issue
Block a user