forked from PluralFlux/PluralFlux
Compare commits
118 Commits
more-disco
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a88804ad | |||
| 2b31cc2ae9 | |||
| df80eca0ec | |||
| 7fead5e3d7 | |||
| 428310dfad | |||
| dc0de4b092 | |||
| 6898e3142c | |||
| adcb05a38b | |||
|
|
db3c588745 | ||
|
|
56d7f7d1fa | ||
|
|
8f6ae668b0 | ||
|
|
211a705f31 | ||
|
|
69c242350f | ||
| 8fc590c062 | |||
|
|
d24bcc8438 | ||
|
|
4f2e893491 | ||
|
|
74cab4f91f | ||
|
|
1661dfd637 | ||
|
|
055ecdf20d | ||
|
|
51ada567ce | ||
|
|
be5505b03c | ||
|
|
07d4e735eb | ||
|
|
8b709b75ce | ||
|
|
6cd34e9c68 | ||
|
|
b2fc9f9111 | ||
|
|
b3813d771d | ||
|
|
f19a5ba58c | ||
|
|
5a9a2977c6 | ||
|
|
b15262e1c5 | ||
|
|
4ea0a777af | ||
|
|
a3c3eb1545 | ||
|
|
559da55176 | ||
|
|
f169f4d755 | ||
|
|
32adf7b6ef | ||
|
|
1db4ab37dc | ||
|
|
fa0de17724 | ||
|
|
99bfdf685b | ||
|
|
0a7057ef63 | ||
|
|
58912f382c | ||
|
|
375601cca7 | ||
|
|
e67a10fa79 | ||
|
|
b83325785f | ||
|
|
49cab523f0 | ||
|
|
e1dbba9043 | ||
|
|
fa86606fb4 | ||
|
|
cf37508ee4 | ||
|
|
38105d910e | ||
|
|
fe05b93ac7 | ||
|
|
964b5ec32a | ||
|
|
cebf14a2f5 | ||
|
|
dafbafeec6 | ||
|
|
8f70960079 | ||
|
|
75dba413b8 | ||
|
|
f81da5ac27 | ||
|
|
be83e8d629 | ||
|
|
9bff32456c | ||
|
|
ba623b0b78 | ||
|
|
85d876f660 | ||
|
|
db5a7398ce | ||
|
|
ff84a637b2 | ||
|
|
8762e41a0a | ||
|
|
3512f07def | ||
|
|
e08ae7b8ae | ||
|
|
63594c9819 | ||
|
|
fc7309630f | ||
|
|
5d80895918 | ||
|
|
2e63532dc7 | ||
|
|
5c1d974246 | ||
|
|
dedd50adfc | ||
|
|
d512e11682 | ||
|
|
be864a4d1b | ||
|
|
7264fe0b1c | ||
|
|
33ddc58c41 | ||
|
|
e729cc770b | ||
|
|
5c6b76b59b | ||
|
|
f0a36f430c | ||
|
|
625be55328 | ||
|
|
163bad02df | ||
|
|
9919e8c4b1 | ||
|
|
70ee8180ab | ||
|
|
55e34b1178 | ||
|
|
e88f66b2dc | ||
|
|
647474e2c2 | ||
|
|
7ef2adc689 | ||
|
|
83a2497ff4 | ||
|
|
89fe2c70b2 | ||
|
|
7c7b1f0202 | ||
|
|
3d36e6c9fc | ||
|
|
623d10a17e | ||
|
|
a2c0530118 | ||
|
|
68629fd75d | ||
|
|
6339da1592 | ||
|
|
d2007f5274 | ||
|
|
c894002018 | ||
|
|
3e0ba190c1 | ||
|
|
12e93ce69d | ||
|
|
415bf44f57 | ||
|
|
e602eefc5d | ||
|
|
650f39266b | ||
|
|
bbc566b8b9 | ||
|
|
62ef2b87cf | ||
|
|
bf155d28b4 | ||
|
|
d473a5119a | ||
|
|
e634085627 | ||
|
|
3bec40e235 | ||
|
|
ba91ecd097 | ||
|
|
23d7abef5d | ||
|
|
0708622045 | ||
|
|
0d129f23e7 | ||
|
|
851bae4ff8 | ||
|
|
a92f00e545 | ||
|
|
8b3be16cea | ||
|
|
1d0c8ef4e7 | ||
|
|
f7a7906532 | ||
|
|
b3f565bd83 | ||
|
|
9b73997c05 | ||
|
|
1b81e4007b | ||
|
|
64535b6a63 |
@@ -1,5 +1,5 @@
|
|||||||
**/.dockerignore
|
**/.dockerignore
|
||||||
**/.env
|
.env.jest
|
||||||
**/.git
|
**/.git
|
||||||
**/.gitignore
|
**/.gitignore
|
||||||
**/.project
|
**/.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
|
||||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: pieartsy
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,5 +1,13 @@
|
|||||||
node_modules
|
node_modules/
|
||||||
|
build/
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
.idea
|
.idea
|
||||||
secrets/
|
secrets/
|
||||||
package-lock.json
|
coverage
|
||||||
config.json
|
config.json
|
||||||
|
log.txt
|
||||||
|
.env
|
||||||
|
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.
|
||||||
25
Dockerfile
25
Dockerfile
@@ -1,21 +1,10 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
|
FROM node:20-alpine AS builder
|
||||||
USER $APP_UID
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
FROM node:20-alpine
|
||||||
ARG BUILD_CONFIGURATION=Release
|
WORKDIR /app
|
||||||
WORKDIR /src
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY ["PluralFlux.csproj", "./"]
|
|
||||||
RUN dotnet restore "PluralFlux.csproj"
|
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/"
|
CMD ["npm", "start"]
|
||||||
RUN dotnet build "./PluralFlux.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "./PluralFlux.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "PluralFlux.dll"]
|
|
||||||
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.
|
||||||
|
|
||||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# PluralFlux
|
||||||
|
PluralFlux is a proxybot akin to PluralKit and Tupperbox, but for [Fluxer](https://fluxer.app/). It is written with the [Fluxer.js](https://fluxerjs.blstmo.com/) library.
|
||||||
|
|
||||||
|
[Invite it to your server](https://web.fluxer.app/oauth2/authorize?client_id=1471588659706540815&scope=bot&permissions=4503600164498496).
|
||||||
|
|
||||||
|
[Join the support server](https://fluxer.gg/WaO6qGdU)
|
||||||
|
|
||||||
|
[Sponsor the project](https://github.com/sponsors/pieartsy)
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
All commands are prefixed by `pf;`. Currently only a few are implemented.
|
||||||
|
|
||||||
|
- `pf;help` - Sends the current list of commands.
|
||||||
|
|
||||||
|
- `pf;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.**"
|
||||||
|
|
||||||
|
- `pf;member` - Accesses the sub-commands related to editing proxy members. The available subcommands are:
|
||||||
|
- `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. The order of values is `pf;member new [name] [displayname] [proxy] [propic]`, _without brackets_. The name is **required**, but the rest are optional.
|
||||||
|
Usage notes:
|
||||||
|
- If anything has spaces, put it in quotes: `"Jane Doe"`
|
||||||
|
- 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.
|
||||||
|
- The maximum length of a display name is 32 characters.
|
||||||
|
- You can't use the same proxy for two different members.
|
||||||
|
- You can also upload an image directly instead of using a url.
|
||||||
|
Examples:
|
||||||
|
- Full example: `pf;member new jane "Jane Doe" J:text https://cdn.pixabay.com/photo/2023/10/20/19/07/aster-8330078_1280.jpg`
|
||||||
|
- 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`.
|
||||||
|
- `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.
|
||||||
|
- `list` - Lists all members in the system.
|
||||||
|
- `displayname` - 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__.
|
||||||
|
- `propic` - Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:
|
||||||
|
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 A{text}` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**
|
||||||
|
|
||||||
|
## Upcoming
|
||||||
|
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.
|
||||||
8
babel.config.js
Normal file
8
babel.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// babel.config.js
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
plugins: ["@babel/plugin-transform-modules-commonjs"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
78
bot.js
78
bot.js
@@ -1,78 +0,0 @@
|
|||||||
import { Client, GatewayDispatchEvents } from "@discordjs/core";
|
|
||||||
import { REST } from "@discordjs/rest";
|
|
||||||
import { WebSocketManager } from "@discordjs/ws";
|
|
||||||
import { db } from './sequelize.js';
|
|
||||||
import { webhookHelper } from "./helpers/webhookHelper.js";
|
|
||||||
import { messageHelper } from "./helpers/messageHelper.js";
|
|
||||||
import { memberHelper } from "./helpers/memberHelper.js";
|
|
||||||
import {enums} from "./enums.js";
|
|
||||||
|
|
||||||
const token = process.env.FLUXER_BOT_TOKEN;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rest = new REST({
|
|
||||||
api: "https://api.fluxer.app",
|
|
||||||
version: "1",
|
|
||||||
}).setToken(token);
|
|
||||||
|
|
||||||
const gateway = new WebSocketManager({
|
|
||||||
token,
|
|
||||||
intents: 0, // Fluxer has no intents yet
|
|
||||||
rest,
|
|
||||||
version: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const client = new Client({ rest, gateway });
|
|
||||||
|
|
||||||
let pluralFluxName = "PluralFlux";
|
|
||||||
let pluralFluxDiscriminator = "8677";
|
|
||||||
|
|
||||||
client.on(GatewayDispatchEvents.MessageCreate, async ({ api, data }) => {
|
|
||||||
try {
|
|
||||||
if (data.webhook_id) {
|
|
||||||
return;
|
|
||||||
} else if (data.author.username === pluralFluxName && data.author.discriminator === pluralFluxDiscriminator) {
|
|
||||||
return;
|
|
||||||
} else if (data.content.startsWith(messageHelper.prefix)) {
|
|
||||||
|
|
||||||
const commandName = data.content.slice(messageHelper.prefix.length).split(" ")[0];
|
|
||||||
const args = messageHelper.parseCommandArgs(data.content, commandName);
|
|
||||||
if (!commandName) {
|
|
||||||
return await api.channels.createMessage(data.channel_id, {content: enums.help.PLURALFLUX});
|
|
||||||
}
|
|
||||||
switch (commandName) {
|
|
||||||
case 'm':
|
|
||||||
case 'member':
|
|
||||||
const attachment = data.attachments[0] ?? null;
|
|
||||||
const reply = await memberHelper.parseMemberCommand(data.author.id, args, attachment);
|
|
||||||
return await api.channels.createMessage(data.channel_id, {content: reply});
|
|
||||||
case 'help':
|
|
||||||
return await api.channels.createMessage(data.channel_id, {content: enums.help.PLURALFLUX});
|
|
||||||
default:
|
|
||||||
return await api.channels.createMessage(data.channel_id, {content: enums.err.NO_SUCH_COMMAND});
|
|
||||||
}
|
|
||||||
const proxyMatch = await messageHelper.parseProxyTags(data.author.id, data.content);
|
|
||||||
if (!proxyMatch.proxy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const member = await memberHelper.getMemberByProxy(data.author.id, proxyMatch.proxy);
|
|
||||||
await webhookHelper.replaceMessage(api, data, proxyMatch.message, member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(error) {
|
|
||||||
return await api.channels.createMessage(data.channel_id, {content: error});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on(GatewayDispatchEvents.Ready, async ({data}) => {
|
|
||||||
console.log(`Logged in as ${data.user.username}#${data.user.discriminator}`);
|
|
||||||
pluralFluxName = data.user.username;
|
|
||||||
pluralFluxDiscriminator = data.user.discriminator;
|
|
||||||
await db.check_connection();
|
|
||||||
});
|
|
||||||
|
|
||||||
await gateway.connect();
|
|
||||||
41
compose.yaml
41
compose.yaml
@@ -1,33 +1,26 @@
|
|||||||
services:
|
services:
|
||||||
|
main:
|
||||||
|
image: engineering.sanya.gay/pluralflux/pluralflux
|
||||||
|
container_name: pluralflux
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: "variables.env"
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
container_name: pluralflux-postgres
|
env_file: "variables.env"
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pwd
|
|
||||||
secrets:
|
|
||||||
- postgres_pwd
|
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql
|
- pgdata:/var/lib/postgresql
|
||||||
|
- ./pgBackup:/mnt/pgBackup
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
# pgadmin:
|
pgadmin:
|
||||||
# image: dpage/pgadmin4:latest
|
image: dpage/pgadmin4:latest
|
||||||
# ports:
|
ports:
|
||||||
# - 5050:80
|
- "5050:80"
|
||||||
# environment:
|
env_file: "variables.env"
|
||||||
# # Required by pgAdmin
|
depends_on:
|
||||||
# PGADMIN_DEFAULT_EMAIL: pieartsy@pm.me
|
- postgres
|
||||||
# PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/postgres_pwd
|
volumes:
|
||||||
# # Don't require the user to login
|
- pgadmindata:/var/lib/pgadmin
|
||||||
# PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
||||||
# # Don't require a "master" password after logging in
|
|
||||||
# PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
|
||||||
# secrets:
|
|
||||||
# - postgres_pwd
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
pgadmindata:
|
||||||
secrets:
|
|
||||||
postgres_pwd:
|
|
||||||
file: ./secrets/postgres-password.txt
|
|
||||||
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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
enums.js
27
enums.js
@@ -1,27 +0,0 @@
|
|||||||
const helperEnums = {};
|
|
||||||
|
|
||||||
helperEnums.err = {
|
|
||||||
NO_MEMBER: "No such member was found.",
|
|
||||||
NO_NAME_PROVIDED: "No member name was provided for",
|
|
||||||
NO_VALUE: "has not been set for this member. Please provide a value.",
|
|
||||||
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.",
|
|
||||||
DISPLAY_NAME_TOO_LONG: "The display name is too long. Please limit it to 32 characters or less.",
|
|
||||||
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 or PNG format.",
|
|
||||||
PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL."
|
|
||||||
}
|
|
||||||
|
|
||||||
helperEnums.help = {
|
|
||||||
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.\n\nThe current commands are: `pf;member` and `pf;help`.",
|
|
||||||
MEMBER: "You can shorten this command to `pf;m`. The available subcommands for `pf;member` are `add`, `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.",
|
|
||||||
ADD: "Creates a new member to proxy with, for example: `pf;member jane`. The member name should ideally be short so you can write other commands with it. \n\nYou can optionally add a display name after the member name, for example: `pf;member new jane \"Jane Doe | ze/hir\"`. If it has spaces, put it in **double quotes**. The length limit is 32 characters.",
|
|
||||||
REMOVE: "Removes a member based on their name, for example: `pf;member remove jane`.",
|
|
||||||
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 quotes.",
|
|
||||||
PROXY: "Updates the proxy tag for a specific member based on their name, for example: `pf;member jane proxy Jane:` or `pf;member amal proxy A=`. This is put at *the start* of a message to allow it to be proxied. Proxies that wrap around text or go at the end are *not* currently supported.",
|
|
||||||
PROPIC: "Updates the profile picture for the member. Must be in JPG or PNG format. 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.",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enums = helperEnums;
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import { db } from '../sequelize.js';
|
|
||||||
import {enums} from "../enums.js";
|
|
||||||
import { loadImage } from "canvas";
|
|
||||||
|
|
||||||
const mh = {};
|
|
||||||
|
|
||||||
// Has an empty "command" to parse the help message properly
|
|
||||||
const commandList = ['--help', 'add', 'remove', 'displayName', 'proxy', 'propic', ''];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses through the subcommands that come after "pf;member" and calls functions accordingly.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @param {string} attachment - The message attachments
|
|
||||||
* @returns {Promise<string>} A message.
|
|
||||||
*/
|
|
||||||
mh.parseMemberCommand = async function(authorId, args, attachment){
|
|
||||||
console.log(authorId, args);
|
|
||||||
let member;
|
|
||||||
// checks whether command is in list, otherwise assumes it's a name
|
|
||||||
if(!commandList.includes(args[0])) {
|
|
||||||
member = await getMemberInfo(authorId, args[0]);
|
|
||||||
if (member === enums.err.NO_MEMBER) {
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch(args[0]) {
|
|
||||||
case '--help':
|
|
||||||
return enums.help.MEMBER;
|
|
||||||
case 'add':
|
|
||||||
return await addNewMember(authorId, args);
|
|
||||||
case 'remove':
|
|
||||||
return await removeMember(authorId, args);
|
|
||||||
case 'displayname':
|
|
||||||
return enums.help.DISPLAY_NAME;
|
|
||||||
case 'proxy':
|
|
||||||
return enums.help.PROXY;
|
|
||||||
case 'propic':
|
|
||||||
return enums.help.PROPIC;
|
|
||||||
case '':
|
|
||||||
return enums.help.MEMBER;
|
|
||||||
}
|
|
||||||
switch(args[1]) {
|
|
||||||
case '--help':
|
|
||||||
return enums.help.MEMBER;
|
|
||||||
case 'displayname':
|
|
||||||
return await updateDisplayName(authorId, args);
|
|
||||||
case 'proxy':
|
|
||||||
return await updateProxy(authorId, args);
|
|
||||||
case 'propic':
|
|
||||||
return await updatePropic(authorId, args, attachment)
|
|
||||||
default:
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a member, first checking that there is no member of that name associated with the author.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @returns {Promise<string>} A successful addition, or an error message.
|
|
||||||
*/
|
|
||||||
async function addNewMember(authorId, args) {
|
|
||||||
if (args[1] && args[1] === "--help" || !args[1]) {
|
|
||||||
return enums.help.ADD;
|
|
||||||
}
|
|
||||||
const memberName = args[1];
|
|
||||||
const displayName = args[2];
|
|
||||||
|
|
||||||
const member = await getMemberInfo(authorId, memberName);
|
|
||||||
if (member && member !== enums.err.NO_MEMBER) {
|
|
||||||
return enums.err.MEMBER_EXISTS;
|
|
||||||
}
|
|
||||||
const trimmedName = displayName ? displayName.replaceAll(' ', '') : null;
|
|
||||||
return await db.members.create({
|
|
||||||
name: memberName,
|
|
||||||
userid: authorId,
|
|
||||||
displayname: trimmedName !== null ? displayName : null,
|
|
||||||
}).then((m) => {
|
|
||||||
let success = `Member was successfully added.\nName: ${m.dataValues.name}`
|
|
||||||
success += displayName ? `\nDisplay name: ${m.dataValues.displayname}` : "";
|
|
||||||
return success;
|
|
||||||
}).catch(e => {
|
|
||||||
return `${enums.err.ADD_ERROR}: ${e.message}`;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the display name for a member.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @returns {Promise<string>} A successful update, or an error message.
|
|
||||||
*/
|
|
||||||
async function updateDisplayName(authorId, args) {
|
|
||||||
if (args[1] && args[1] === "--help" || !args[1]) {
|
|
||||||
return enums.help.DISPLAY_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberName = args[0];
|
|
||||||
const displayName = args[2];
|
|
||||||
const trimmed_name = displayName ? displayName.replaceAll(' ', '') : null;
|
|
||||||
|
|
||||||
if (!displayName || trimmed_name === null ) {
|
|
||||||
let member = await mh.getMemberByName(authorId, memberName);
|
|
||||||
if (member.displayname) {
|
|
||||||
return `Display name for ${memberName} is: \"${member.displayname}\".`;
|
|
||||||
}
|
|
||||||
return `Display name ${enums.err.NO_VALUE}`
|
|
||||||
}
|
|
||||||
else if (displayName.length > 32) {
|
|
||||||
return enums.err.DISPLAY_NAME_TOO_LONG;
|
|
||||||
}
|
|
||||||
console.log(displayName);
|
|
||||||
return updateMember(authorId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the proxy for a member, first checking that no other members attached to the author have the tag.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @returns {Promise<string> } A successful update, or an error message.
|
|
||||||
*/
|
|
||||||
async function updateProxy(authorId, args) {
|
|
||||||
if (args[1] && args[1] === "--help" || !args[1]) {
|
|
||||||
return enums.help.PROXY;
|
|
||||||
}
|
|
||||||
const proxy = args[2];
|
|
||||||
const trimmedProxy = proxy ? proxy.replaceAll(' ', '') : null;
|
|
||||||
|
|
||||||
if (trimmedProxy == null) {
|
|
||||||
return `Proxy ${enums.err.NO_VALUE}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = await mh.getMembersByAuthor(authorId);
|
|
||||||
const proxyExists = members.some(member => member.proxy === proxy);
|
|
||||||
if (proxyExists) {
|
|
||||||
return enums.err.PROXY_EXISTS;
|
|
||||||
}
|
|
||||||
return updateMember(authorId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the profile pic for a member, based on either the attachment or the args provided.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @param {string} attachment - The url of the first attachment in the message
|
|
||||||
* @returns {Promise<string> } A successful update, or an error message.
|
|
||||||
*/
|
|
||||||
async function updatePropic(authorId, args, attachment) {
|
|
||||||
if (args[1] && args[1] === "--help") {
|
|
||||||
return enums.help.PROPIC;
|
|
||||||
}
|
|
||||||
let img;
|
|
||||||
const updatedArgs = args;
|
|
||||||
if (!updatedArgs[1] && !attachment) {
|
|
||||||
return enums.help.PROPIC;
|
|
||||||
} else if (attachment) {
|
|
||||||
updatedArgs[2] = attachment.url;
|
|
||||||
updatedArgs[3] = attachment.expires_at;
|
|
||||||
}
|
|
||||||
if (updatedArgs[2]) {
|
|
||||||
img = updatedArgs[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return await loadImage(img).then(() => {
|
|
||||||
return updateMember(authorId, updatedArgs);
|
|
||||||
}).catch((err) => {
|
|
||||||
return `${enums.err.PROPIC_CANNOT_LOAD}: ${err.message}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a member.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @returns {Promise<string>} A successful removal, or an error message.
|
|
||||||
*/
|
|
||||||
async function removeMember(authorId, args) {
|
|
||||||
if (args[1] && args[1] === "--help") {
|
|
||||||
return enums.help.REMOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberName = args[1];
|
|
||||||
if (!memberName) {
|
|
||||||
return `${enums.err.NO_NAME_PROVIDED} deletion.`;
|
|
||||||
}
|
|
||||||
return await db.members.destroy({ where: { name: memberName, userid: authorId } }).then(() => {
|
|
||||||
return `Member "${memberName}" has been deleted.`;
|
|
||||||
}).catch(e => {
|
|
||||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*======Non-Subcommands======*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a member's fields in the database.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string[]} args - The message arguments
|
|
||||||
* @returns {Promise<string>} A successful update, or an error message.
|
|
||||||
*/
|
|
||||||
async function updateMember(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]) {
|
|
||||||
console.log(args);
|
|
||||||
fluxerPropicWarning = setExpirationWarning(args[3]);
|
|
||||||
}
|
|
||||||
return await db.members.update({[columnName]: value}, { where: { name: memberName, userid: authorId } }).then(() => {
|
|
||||||
return `Updated ${columnName} for ${memberName} to "${value}"${fluxerPropicWarning ?? ''}.`;
|
|
||||||
}).catch(e => {
|
|
||||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the warning for an expiration date.
|
|
||||||
*
|
|
||||||
* @param {string} expirationString - An expiration date string.
|
|
||||||
* @returns {string} A successful update, or an error message.
|
|
||||||
*/
|
|
||||||
function setExpirationWarning(expirationString) {
|
|
||||||
let expirationDate = new Date(expirationString);
|
|
||||||
console.log(expirationDate, expirationDate instanceof Date);
|
|
||||||
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.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the details for a member.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string} memberName - The message arguments
|
|
||||||
* @returns {Promise<string>} The member's info, or an error message.
|
|
||||||
*/
|
|
||||||
async function getMemberInfo(authorId, memberName) {
|
|
||||||
let member = await db.members.findOne({ where: { name: memberName, userid: authorId } });
|
|
||||||
if (member) {
|
|
||||||
let member_info = `Member name: ${member.name}`;
|
|
||||||
member_info += member.displayname ? `\nDisplay name: ${member.displayname}` : '\nDisplay name: unset';
|
|
||||||
member_info += member.proxy ? `\nProxy Tag: ${member.proxy}` : '\nProxy tag: unset';
|
|
||||||
member_info += member.propic ? `\nProfile pic: ${member.propic}` : '\nProfile pic: unset';
|
|
||||||
return member_info;
|
|
||||||
}
|
|
||||||
return enums.err.NO_MEMBER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a member based on the author and proxy tag.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message.
|
|
||||||
* @param {string} name - The member's name.
|
|
||||||
* @returns {Promise<model> | Promise<string>} The member object, or an error message.
|
|
||||||
*/
|
|
||||||
mh.getMemberByName = async function(authorId, name) {
|
|
||||||
return await db.members.findOne({ where: { userid: authorId, name: name } }).catch(e => {
|
|
||||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a member based on the author and proxy tag.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @param {string} proxy - The proxy tag
|
|
||||||
* @returns {Promise<model> | Promise<string>} The member object, or an error message.
|
|
||||||
*/
|
|
||||||
mh.getMemberByProxy = async function(authorId, proxy) {
|
|
||||||
return await db.members.findOne({ where: { userid: authorId, proxy: proxy } }).catch(e => {
|
|
||||||
return `${enums.err.NO_MEMBER}: ${e.message}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all members belonging to the author.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message
|
|
||||||
* @returns {Promise<model[]> | Promise<string>} The member object, or an error message.
|
|
||||||
*/
|
|
||||||
mh.getMembersByAuthor = async function(authorId) {
|
|
||||||
return await db.members.findAll({ where: { userid: authorId } }).catch(e => {
|
|
||||||
// I have no idea how this could possibly happen but better safe than sorry
|
|
||||||
return `${enums.err.USER_NO_MEMBERS}: ${e.message}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const memberHelper = mh;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import {memberHelper} from "./memberHelper.js";
|
|
||||||
|
|
||||||
const msgh = {};
|
|
||||||
|
|
||||||
msgh.prefix = "pf;"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and slices up message arguments, retaining quoted strings.
|
|
||||||
*
|
|
||||||
* @param {string} text - The full message content.
|
|
||||||
* @param {string} commandName - The command name.
|
|
||||||
* @returns {string[]} An array of arguments.
|
|
||||||
*/
|
|
||||||
msgh.parseCommandArgs = function(text, commandName) {
|
|
||||||
const message = text.slice(msgh.prefix.length + commandName.length).trim();
|
|
||||||
|
|
||||||
return message.match(/\\?.|^$/g).reduce((accumulator, chara) => {
|
|
||||||
if (chara === '"') {
|
|
||||||
// checks whether string is within quotes or not
|
|
||||||
accumulator.quote ^= 1;
|
|
||||||
} else if (!accumulator.quote && chara === ' '){
|
|
||||||
// if not currently in quoted string, push empty string to start word
|
|
||||||
accumulator.array.push('');
|
|
||||||
} else {
|
|
||||||
// accumulates characters to the last string in the array and removes escape characters
|
|
||||||
accumulator.array[accumulator.array.length-1] += chara.replace(/\\(.)/,"$1");
|
|
||||||
}
|
|
||||||
return accumulator;
|
|
||||||
}, {array: ['']}).array // initial array with empty string for the reducer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses proxy tags and sees if they match the tags of any member belonging to an author.
|
|
||||||
*
|
|
||||||
* @param {string} authorId - The author of the message.
|
|
||||||
* @param {string} text - The full message content.
|
|
||||||
* @returns {Object} The proxy message object.
|
|
||||||
*/
|
|
||||||
msgh.parseProxyTags = async function (authorId, text){
|
|
||||||
const members = await memberHelper.getMembersByAuthor(authorId);
|
|
||||||
const proxyMessage = {}
|
|
||||||
members.forEach(member => {
|
|
||||||
if (text.startsWith(member.proxy) && text.length > member.proxy.length) {
|
|
||||||
proxyMessage.proxy = member.proxy;
|
|
||||||
proxyMessage.message = text.slice(member.proxy.length).trim();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return proxyMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const messageHelper = msgh;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
const wh = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets or creates a webhook.
|
|
||||||
*
|
|
||||||
* @param api - The discord.js API.
|
|
||||||
* @param {string} channelId - The channel the message was sent in.
|
|
||||||
* @returns {Object} A webhook object.
|
|
||||||
*/
|
|
||||||
wh.getOrCreateWebhook = async function (api, channelId) {
|
|
||||||
const name = 'PluralFlux Proxy Webhook';
|
|
||||||
let webhook = await getWebhook(api, channelId, name);
|
|
||||||
if (!webhook) {
|
|
||||||
webhook = await api.channels.createWebhook(channelId, {name: name});
|
|
||||||
}
|
|
||||||
return webhook;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an existing webhook.
|
|
||||||
*
|
|
||||||
* @param api - The discord.js API.
|
|
||||||
* @param {string} channelId - The channel the message was sent in.
|
|
||||||
* @param {string} name - The name of the webhook.
|
|
||||||
* @returns {Object} A webhook object.
|
|
||||||
*/
|
|
||||||
async function getWebhook(api, channelId, name) {
|
|
||||||
const allWebhooks = await api.channels.getWebhooks(channelId);
|
|
||||||
if (allWebhooks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let pf_webhook;
|
|
||||||
allWebhooks.forEach((webhook) => {
|
|
||||||
if (webhook.name === name) {
|
|
||||||
pf_webhook = webhook;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return pf_webhook;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces a proxied message with a webhook using the member information.
|
|
||||||
*
|
|
||||||
* @param api - The discord.js API.
|
|
||||||
* @param data - The discord.js data.
|
|
||||||
* @param {string} text - The text to send via the webhook.
|
|
||||||
* @param {Object} member - A member object from the database.
|
|
||||||
*/
|
|
||||||
wh.replaceMessage = async function (api, data, text, member) {
|
|
||||||
if (text.length > 0) {
|
|
||||||
const webhook = await wh.getOrCreateWebhook(api, data.channel_id);
|
|
||||||
await api.webhooks.execute(webhook.id, webhook.token, {content: text, username: member.displayname ?? member.name, avatar_url: member.propic});
|
|
||||||
await api.channels.deleteMessage(data.channel_id, data.id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await api.channels.createMessage(data.channel_id, {content: '(Please input a message!)'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const webhookHelper = wh;
|
|
||||||
10
jest.config.js
Normal file
10
jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
clearMocks: true,
|
||||||
|
collectCoverage: true,
|
||||||
|
coverageDirectory: "coverage",
|
||||||
|
verbose: true,
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[t|j]sx?$": require.resolve('babel-jest')
|
||||||
|
},
|
||||||
|
};
|
||||||
8846
package-lock.json
generated
Normal file
8846
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -2,24 +2,41 @@
|
|||||||
"name": "pluralflux",
|
"name": "pluralflux",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "bot.js",
|
"main": "src/bot.js",
|
||||||
"type": "module",
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/pieartsy/PluralFlux.git"
|
"url": "https://github.com/pieartsy/PluralFlux.git"
|
||||||
},
|
},
|
||||||
|
"type": "commonjs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/core": "^2.4.0",
|
"@fluxerjs/core": "^1.2.2",
|
||||||
"@discordjs/rest": "^2.6.0",
|
"dotenv": "^17.3.1",
|
||||||
"@discordjs/ws": "^2.0.4",
|
"pg": "^8.19.0",
|
||||||
"canvas": "^3.2.1",
|
|
||||||
"pg": "^8.18.0",
|
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
"sequelize": "^6.37.7"
|
"pm2": "^6.0.14",
|
||||||
|
"psql": "^0.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"tmp": "^0.2.5",
|
||||||
|
"typeorm": "^0.3.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@babel/core": "^7.29.0",
|
||||||
"eslint": "^10.0.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",
|
||||||
|
"fetch-mock": "^12.6.0",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
sequelize.js
59
sequelize.js
@@ -1,59 +0,0 @@
|
|||||||
import {DataTypes, Sequelize} from 'sequelize';
|
|
||||||
|
|
||||||
const password = process.env.POSTGRES_PASSWORD;
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
console.error("Missing POSTGRES_PWD environment variable.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const database = {};
|
|
||||||
|
|
||||||
const sequelize = new Sequelize('postgres', 'postgres', password, {
|
|
||||||
host: 'localhost',
|
|
||||||
dialect: 'postgres'
|
|
||||||
});
|
|
||||||
|
|
||||||
database.sequelize = sequelize;
|
|
||||||
database.Sequelize = Sequelize;
|
|
||||||
|
|
||||||
database.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,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
database.check_connection = async function() {
|
|
||||||
await sequelize.authenticate().then(async (result) => {
|
|
||||||
console.log('Connection has been established successfully.');
|
|
||||||
await syncModels();
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Unable to connect to the database:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncModels() {
|
|
||||||
await sequelize.sync().then((result) => {
|
|
||||||
console.log('Models synced successfully.');
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error('Syncing models did not work', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const db = database;
|
|
||||||
104
src/bot.js
Normal file
104
src/bot.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
client = new Client({ intents: 0 });
|
||||||
|
|
||||||
|
module.exports.client = client;
|
||||||
|
|
||||||
|
client.on(Events.MessageCreate, async (message) => {
|
||||||
|
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)) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
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, args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await message.reply(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.on(Events.Ready, () => {
|
||||||
|
console.log(`Logged in as ${client.user?.username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let guildCount = 0;
|
||||||
|
client.on(Events.GuildCreate, () => {
|
||||||
|
guildCount++;
|
||||||
|
debouncePrintGuilds();
|
||||||
|
});
|
||||||
|
|
||||||
|
function printGuilds() {
|
||||||
|
console.log(`Serving ${client.guilds.size} guild(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncePrintGuilds = utils.debounce(printGuilds, 2000);
|
||||||
|
// export const debounceLogin = utils.debounce(client.login, 60000);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main()
|
||||||
|
{
|
||||||
|
exports.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
122
src/commands.js
Normal file
122
src/commands.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
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 commands = {
|
||||||
|
commandsMap: new Map(),
|
||||||
|
aliasesMap: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.aliasesMap.set('m', {command: 'member'})
|
||||||
|
|
||||||
|
commands.commandsMap.set('member', {
|
||||||
|
description: enums.help.SHORT_DESC_MEMBER,
|
||||||
|
async execute(message, args) {
|
||||||
|
await commands.memberCommand(message, args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = [...commands.commandsMap.entries()].map(([name, cmd]) => ({
|
||||||
|
name: `${messageHelper.prefix}${name}`,
|
||||||
|
value: cmd.description,
|
||||||
|
inline: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('Commands')
|
||||||
|
.setDescription(enums.help.PLURALFLUX)
|
||||||
|
.addFields(...fields)
|
||||||
|
.setFooter({text: `Prefix: ${messageHelper.prefix}`})
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await message.reply({embeds: [embed]});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
commands.commandsMap.set('import', {
|
||||||
|
description: enums.help.SHORT_DESC_IMPORT,
|
||||||
|
async execute(message, args) {
|
||||||
|
await commands.importCommand(message, args);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
53
src/enums.js
Normal file
53
src/enums.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const enums = {};
|
||||||
|
|
||||||
|
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.",
|
||||||
|
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. 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.',
|
||||||
|
NO_TEXT_FOR_PROXY: "You need the word 'text' for the bot to detect proxy tags with.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`",
|
||||||
|
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.',
|
||||||
|
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.",
|
||||||
|
CANNOT_FETCH_RESOURCE: "Could not download the file at this time."
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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.**",
|
||||||
|
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 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.**\n\n**PRO TIP**: For privacy reasons, try DMing the bot with this command and your JSON file--it should still work the same."
|
||||||
|
}
|
||||||
|
|
||||||
|
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/"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.enums = enums;
|
||||||
58
src/helpers/importHelper.js
Normal file
58
src/helpers/importHelper.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const {enums} = require("../enums.js");
|
||||||
|
const {memberHelper} = require("./memberHelper.js");
|
||||||
|
|
||||||
|
const importHelper = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to import from Pluralkit.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
importHelper.pluralKitImport = async function (authorId, attachmentUrl= null) {
|
||||||
|
let fetchResult, pkData;
|
||||||
|
if (!attachmentUrl) {
|
||||||
|
throw new Error(enums.err.NOT_JSON_FILE);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importHelper = importHelper;
|
||||||
484
src/helpers/memberHelper.js
Normal file
484
src/helpers/memberHelper.js
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
const {enums} = require("../enums.js");
|
||||||
|
const {EmbedBuilder} = require("@fluxerjs/core");
|
||||||
|
const {utils} = require("./utils.js");
|
||||||
|
const {memberRepo} = require("../repositories/memberRepo.js");
|
||||||
|
|
||||||
|
const memberHelper = {};
|
||||||
|
|
||||||
|
const commandList = ['new', 'remove', 'name', 'list', 'displayname', 'proxy', 'propic'];
|
||||||
|
const newAndRemoveCommands = ['new', 'remove'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
// 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 enums.help.NEW;
|
||||||
|
case 'remove':
|
||||||
|
return enums.help.REMOVE;
|
||||||
|
case 'name':
|
||||||
|
return enums.help.NAME;
|
||||||
|
case 'displayname':
|
||||||
|
return enums.help.DISPLAY_NAME;
|
||||||
|
case 'proxy':
|
||||||
|
return enums.help.PROXY;
|
||||||
|
case 'propic':
|
||||||
|
return enums.help.PROPIC;
|
||||||
|
case 'list':
|
||||||
|
return enums.help.LIST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 memberHelper.updateName(authorId, memberName, values[0]);
|
||||||
|
case 'displayname':
|
||||||
|
return await memberHelper.updateDisplayName(authorId, memberName, values[0]);
|
||||||
|
case 'proxy':
|
||||||
|
return await memberHelper.updateProxy(authorId, memberName, values[0]);
|
||||||
|
case 'propic':
|
||||||
|
return await memberHelper.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration);
|
||||||
|
default:
|
||||||
|
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a member.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
memberHelper.addNewMember = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
|
||||||
|
const displayName = values[0];
|
||||||
|
const proxy = values[1];
|
||||||
|
const propic = values[2] ?? attachmentUrl;
|
||||||
|
|
||||||
|
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.`}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the name for a member.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
memberHelper.updateName = async function (authorId, memberName, name) {
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
if (trimmedName === '') {
|
||||||
|
throw new RangeError(`Name ${enums.err.NO_VALUE}`);
|
||||||
|
}
|
||||||
|
return await memberHelper.updateMemberField(authorId, memberName, "name", trimmedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the display name for a member.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
memberHelper.updateDisplayName = async function (authorId, membername, displayname) {
|
||||||
|
const trimmedName = displayname.trim();
|
||||||
|
|
||||||
|
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 memberHelper.updateMemberField(authorId, membername, "displayname", trimmedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the proxy for a member, first checking that no other members attached to the author have the tag.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @param {string} memberName - The member to update
|
||||||
|
* @param {string} proxy - The proxy to set
|
||||||
|
* @returns {Promise<string> } A successful update.
|
||||||
|
*/
|
||||||
|
memberHelper.updateProxy = async function (authorId, memberName, proxy) {
|
||||||
|
// Throws error if exists
|
||||||
|
await memberHelper.checkIfProxyExists(authorId, proxy);
|
||||||
|
|
||||||
|
return await memberHelper.updateMemberField(authorId, memberName, "proxy", proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the profile pic for a member, based on either the attachment or the args provided.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a member.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @param {string} memberName - The name of the member to remove
|
||||||
|
* @returns {Promise<string>} A successful removal.
|
||||||
|
* @throws {Error} When there is no member to 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*======Non-Subcommands======*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a member with full details, first checking that there is no member of that name associated with the author.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isValidDisplayName = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValidProxy;
|
||||||
|
if (proxy && proxy.length > 0) {
|
||||||
|
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, expirationWarning;
|
||||||
|
if (propic && propic.length > 0) {
|
||||||
|
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 memberRepo.createMember({
|
||||||
|
name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
|
||||||
|
});
|
||||||
|
|
||||||
|
return {member: member, errors: errors};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param {string | null} [expirationWarning] - The attachment expiration warning (if any)
|
||||||
|
* @returns {Promise<string>} A successful update.
|
||||||
|
* @throws {Error} When no member row was updated.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param {{Member, string[]}} member - The member object
|
||||||
|
* @returns {EmbedBuilder} The member's info.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all members for an author.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The id of the message author
|
||||||
|
* @param {string} authorName - The id name the message author
|
||||||
|
* @returns {Promise<EmbedBuilder>} The info for all members.
|
||||||
|
* @throws {Error} When there are no members for an author.
|
||||||
|
*/
|
||||||
|
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.length > 25 ? "First 25 m" : "M"}embers for ${authorName}`)
|
||||||
|
.addFields(...fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if proxy exists for a member.
|
||||||
|
*
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @param {string} proxy - The proxy tag.
|
||||||
|
* @returns {Promise<boolean> } Whether the proxy exists.
|
||||||
|
* @throws {Error} When an empty proxy was provided, or no proxy exists.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports.memberHelper = memberHelper;
|
||||||
83
src/helpers/messageHelper.js
Normal file
83
src/helpers/messageHelper.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const {memberRepo} = require('../repositories/memberRepo.js');
|
||||||
|
|
||||||
|
const msgh = {};
|
||||||
|
|
||||||
|
msgh.prefix = "pf;"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses and slices up message arguments, retaining quoted strings.
|
||||||
|
*
|
||||||
|
* @param {string} content - The full message content.
|
||||||
|
* @param {string} commandName - The command name.
|
||||||
|
* @returns {string[]} An array of arguments.
|
||||||
|
*/
|
||||||
|
msgh.parseCommandArgs = function(content, commandName) {
|
||||||
|
const message = content.slice(msgh.prefix.length + commandName.length).trim();
|
||||||
|
|
||||||
|
return message.match(/\\?.|^$/g).reduce((accumulator, chara) => {
|
||||||
|
if (chara === '\"' || chara === '\'') {
|
||||||
|
// checks whether string is within quotes or not
|
||||||
|
accumulator.quote ^= 1;
|
||||||
|
} else if (!accumulator.quote && chara === ' '){
|
||||||
|
// if not currently in quoted string, push empty string to start word
|
||||||
|
accumulator.array.push('');
|
||||||
|
} else {
|
||||||
|
// accumulates characters to the last string in the array and removes escape characters
|
||||||
|
accumulator.array[accumulator.array.length-1] += chara.replace(/\\(.)/,"$1");
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
}, {array: ['']}).array // initial array with empty string for the reducer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Promise<{model, string, bool}>} The proxy message object.
|
||||||
|
*/
|
||||||
|
msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){
|
||||||
|
const members = await memberRepo.getMembersByAuthor(authorId);
|
||||||
|
// If an author has no members, no sense in searching for proxy
|
||||||
|
if (members.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyMessage = {}
|
||||||
|
members.forEach(member => {
|
||||||
|
if (member.proxy) {
|
||||||
|
const splitProxy = member.proxy.split("text");
|
||||||
|
if(content.startsWith(splitProxy[0]) && content.endsWith(splitProxy[1])) {
|
||||||
|
proxyMessage.member = member;
|
||||||
|
proxyMessage.hasAttachment = !!attachmentUrl;
|
||||||
|
let escapedPrefix = splitProxy[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
let escapedSuffix = splitProxy[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
escapedPrefix = new RegExp("^" + escapedPrefix);
|
||||||
|
escapedSuffix = new RegExp(escapedSuffix + "$")
|
||||||
|
proxyMessage.message = content.replace(escapedPrefix, "").replace(escapedSuffix, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return proxyMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a text message that's too long as its text plus a file with the remaining text.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text of the message.
|
||||||
|
* @returns {{text: string, file: Buffer<ArrayBuffer> | undefined}} The text and buffer object
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
msgh.returnBufferFromText = function (text) {
|
||||||
|
if (text.length > 2000) {
|
||||||
|
const truncated = text.substring(0, 2000);
|
||||||
|
const restOfText = text.substring(2000);
|
||||||
|
const file = Buffer.from(restOfText, 'utf-8');
|
||||||
|
return {text: truncated, file: file}
|
||||||
|
}
|
||||||
|
return {text: text, file: undefined}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
96
src/helpers/webhookHelper.js
Normal file
96
src/helpers/webhookHelper.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const {messageHelper} = require("./messageHelper.js");
|
||||||
|
const {Webhook, Channel, Message, Client} = require('@fluxerjs/core');
|
||||||
|
const {enums} = require("../enums.js");
|
||||||
|
|
||||||
|
const webhookHelper = {};
|
||||||
|
|
||||||
|
const name = 'PluralFlux Proxy Webhook';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a proxied message with a webhook using the member information.
|
||||||
|
* @async
|
||||||
|
* @param {Client} client - The fluxer.js client.
|
||||||
|
* @param {Message} message - The full message object.
|
||||||
|
* @throws {Error} When the proxy message is not in a server.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
// If the message doesn't match a proxy, just return.
|
||||||
|
if (!proxyMatch || !proxyMatch.member || (proxyMatch.message.length === 0 && !proxyMatch.hasAttachment) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the message does match a proxy but is not in a guild server (ex: in the Bot's DMs)
|
||||||
|
if (!message.guildId) {
|
||||||
|
throw new Error(enums.err.NOT_IN_SERVER);
|
||||||
|
}
|
||||||
|
if (proxyMatch.hasAttachment) {
|
||||||
|
return await message.reply(`${enums.misc.ATTACHMENT_SENT_BY} ${proxyMatch.member.displayname ?? proxyMatch.member.name}`)
|
||||||
|
}
|
||||||
|
await webhookHelper.replaceMessage(client, message, proxyMatch.message, proxyMatch.member);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a proxied message with a webhook using the member information.
|
||||||
|
* @async
|
||||||
|
* @param {Client} client - The fluxer.js client.
|
||||||
|
* @param {Message} message - The message to be deleted.
|
||||||
|
* @param {string} text - The text to send via the webhook.
|
||||||
|
* @param {model} member - A member object from the database.
|
||||||
|
* @throws {Error} When there's no message to send.
|
||||||
|
*/
|
||||||
|
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 webhookHelper.getOrCreateWebhook(client, channel);
|
||||||
|
const username = member.displayname ?? member.name;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates a webhook.
|
||||||
|
* @async
|
||||||
|
* @param {Client} client - The fluxer.js client.
|
||||||
|
* @param {Channel} channel - The channel the message was sent in.
|
||||||
|
* @returns {Webhook} A webhook object.
|
||||||
|
* @throws {Error} When no webhooks are allowed in the 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 webhookHelper.getWebhook(client, channel)
|
||||||
|
if (!webhook) {
|
||||||
|
webhook = await channel.createWebhook({name: name});
|
||||||
|
}
|
||||||
|
return webhook;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an existing webhook.
|
||||||
|
* @async
|
||||||
|
* @param {Client} client - The fluxer.js client.
|
||||||
|
* @param {Channel} channel - The channel the message was sent in.
|
||||||
|
* @returns {Webhook} A webhook object.
|
||||||
|
*/
|
||||||
|
webhookHelper.getWebhook = async function(client, channel) {
|
||||||
|
const channelWebhooks = await channel?.fetchWebhooks() ?? [];
|
||||||
|
if (channelWebhooks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return channelWebhooks.find((webhook) => webhook.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
})
|
||||||
737
tests/helpers/memberHelper.test.js
Normal file
737
tests/helpers/memberHelper.test.js
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
const {enums} = require('../../src/enums.js');
|
||||||
|
const {utils} = require("../../src/helpers/utils.js");
|
||||||
|
|
||||||
|
jest.mock('@fluxerjs/core', () => jest.fn());
|
||||||
|
jest.mock('../../src/repositories/memberRepo.js', () => {
|
||||||
|
return {
|
||||||
|
memberRepo: {
|
||||||
|
getMemberByName: jest.fn().mockResolvedValue(),
|
||||||
|
getMembersByAuthor: jest.fn().mockResolvedValue(),
|
||||||
|
removeMember: jest.fn().mockResolvedValue(),
|
||||||
|
createMember: jest.fn().mockResolvedValue(),
|
||||||
|
updateMemberField: jest.fn().mockResolvedValue(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("../../src/helpers/utils.js", () => {
|
||||||
|
return {
|
||||||
|
utils:
|
||||||
|
{
|
||||||
|
checkImageFormatValidity: jest.fn().mockResolvedValue(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {memberHelper} = require("../../src/helpers/memberHelper.js");
|
||||||
|
const {memberRepo} = require("../../src/repositories/memberRepo.js");
|
||||||
|
|
||||||
|
describe('MemberHelper', () => {
|
||||||
|
const authorId = "0001";
|
||||||
|
const authorFull = "author#0001";
|
||||||
|
const attachmentUrl = "../oya.png";
|
||||||
|
const attachmentExpiration = new Date('2026-01-01').toDateString();
|
||||||
|
const mockMember = {
|
||||||
|
name: "somePerson",
|
||||||
|
displayname: "Some Person",
|
||||||
|
proxy: "--text",
|
||||||
|
propic: 'ono.png'
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseMemberCommand', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(memberHelper, 'getMemberCommandInfo').mockResolvedValue("member command info");
|
||||||
|
jest.spyOn(memberHelper, 'memberArgumentHandler').mockResolvedValue("handled argument");
|
||||||
|
jest.spyOn(memberHelper, 'memberCommandHandler').mockResolvedValue("called command");
|
||||||
|
jest.spyOn(memberHelper, 'sendCurrentValue').mockResolvedValue("current value");
|
||||||
|
jest.spyOn(memberHelper, 'sendHelpEnum').mockResolvedValue("help enum")
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[['--help']],
|
||||||
|
[['']],
|
||||||
|
[[]]
|
||||||
|
])('%s calls getMemberCommandInfo and returns expected result', async (args) => {
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.parseMemberCommand(authorId, authorFull, args);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("member command info");
|
||||||
|
expect(memberHelper.getMemberCommandInfo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getMemberCommandInfo).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[[mockMember.name, '--help'], null, null, undefined, true, undefined],
|
||||||
|
[['new', '--help'], null, null, 'new', true, '--help'],
|
||||||
|
[['remove', '--help'], null, null, 'remove', true, '--help'],
|
||||||
|
[['name', '--help'], null, null, 'name', true, '--help'],
|
||||||
|
[['list', '--help'], null, null, 'list', true, '--help'],
|
||||||
|
[['name', '--help'], null, null, 'name', true, '--help'],
|
||||||
|
[['displayname', '--help'], null, null, 'displayname', true, '--help'],
|
||||||
|
[['proxy', '--help'], null, null, 'proxy', true, '--help'],
|
||||||
|
[['propic', '--help'], null, null, 'propic', true, '--help'],
|
||||||
|
[['new'], null, null, 'new', true, undefined],
|
||||||
|
[['remove'], null, null, 'remove', true, undefined],
|
||||||
|
[['name'], null, null, 'name', true, undefined],
|
||||||
|
[['list'], null, null, 'list', false, undefined],
|
||||||
|
[['displayname'], null, null, 'displayname', true, undefined],
|
||||||
|
[['proxy'], null, null, 'proxy', true, undefined],
|
||||||
|
[['propic'], null, null, 'propic', true, undefined],
|
||||||
|
[[mockMember.name, 'remove'], null, null, 'remove', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'remove', 'test'], null, null, 'remove', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new'], null, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname], null, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy], null, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, null], mockMember.propic, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, null], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'name', mockMember.name], null, null, 'name', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', '', mockMember.proxy], null, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', '', '', mockMember.propic], null, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', '', '', null], mockMember.propic, null, 'new', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'new', '', '', null], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name],
|
||||||
|
//
|
||||||
|
[[mockMember.name, 'displayname', mockMember.displayname], null, null, 'displayname', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'proxy', mockMember.proxy], null, null, 'proxy', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'propic', mockMember.propic], null, null, 'propic', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'propic', null], mockMember.propic, null, 'propic', false, mockMember.name],
|
||||||
|
[[mockMember.name, 'propic', null], mockMember.propic, attachmentExpiration, 'propic', false, mockMember.name],
|
||||||
|
[['remove', mockMember.name], null, null, 'remove', false, mockMember.name],
|
||||||
|
[['remove', mockMember.name, 'test'], null, null, 'remove', false, mockMember.name],
|
||||||
|
[['new', mockMember.name], null, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, mockMember.displayname], null, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, '', mockMember.proxy], null, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, '', '', mockMember.propic], null, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, '', '', null], mockMember.propic, null, 'new', false, mockMember.name],
|
||||||
|
[['new', mockMember.name, '', '', null], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name],
|
||||||
|
//
|
||||||
|
[['name', mockMember.name, mockMember.name], null, null, 'name', false, mockMember.name],
|
||||||
|
[['displayname', mockMember.name, mockMember.name, mockMember.displayname], null, null, 'displayname', false, mockMember.name],
|
||||||
|
[['proxy', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'proxy', false, mockMember.name],
|
||||||
|
[['propic', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'propic', false, mockMember.name],
|
||||||
|
[['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'propic', false, mockMember.name],
|
||||||
|
[['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'propic', false, mockMember.name]
|
||||||
|
])('%s args with attachmentURL %s and attachment expiration %s calls memberCommandHandler with correct values', async (args, attachmentUrl, attachmentExpiration, command, isHelp, memberName) => {
|
||||||
|
console.log(args, command, isHelp)
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("handled argument");
|
||||||
|
expect(memberHelper.memberArgumentHandler).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.memberArgumentHandler).toHaveBeenCalledWith(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('memberArgumentHandler', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(memberHelper, 'memberCommandHandler').mockResolvedValue("handled command");
|
||||||
|
jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info");
|
||||||
|
jest.spyOn(memberHelper, 'sendCurrentValue').mockResolvedValue("current value");
|
||||||
|
jest.spyOn(memberHelper, 'sendHelpEnum').mockReturnValue("help enum");
|
||||||
|
})
|
||||||
|
|
||||||
|
test('when all values are null should throw command not recognized enum', async () => {
|
||||||
|
// Arrange
|
||||||
|
await expect(memberHelper.memberArgumentHandler(authorId, authorFull, false, null, null, [])).rejects.toThrow(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['new'],
|
||||||
|
['remove'],
|
||||||
|
['name'],
|
||||||
|
['displayname'],
|
||||||
|
['proxy'],
|
||||||
|
['propic'],
|
||||||
|
])('when %s is present but other values are null, should throw no member enum', async (command) => {
|
||||||
|
// Arrange
|
||||||
|
await expect(memberHelper.memberArgumentHandler(authorId, authorFull, false, command, null, [])).rejects.toThrow(enums.err.NO_MEMBER);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['new'],
|
||||||
|
['remove'],
|
||||||
|
['name'],
|
||||||
|
['list'],
|
||||||
|
['displayname'],
|
||||||
|
['proxy'],
|
||||||
|
['propic'],
|
||||||
|
])('%s calls sendHelpEnum', async (command) => {
|
||||||
|
// Arrange
|
||||||
|
const result = await memberHelper.memberArgumentHandler(authorId, authorFull, true, command, mockMember.name, []);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("help enum");
|
||||||
|
expect(memberHelper.sendHelpEnum).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.sendHelpEnum).toHaveBeenCalledWith(command);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('list should call getAllMembersInfo', async () => {
|
||||||
|
// Arrange
|
||||||
|
const result = await memberHelper.memberArgumentHandler(authorId, authorFull, false, 'list', mockMember.name, []);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("all member info");
|
||||||
|
expect(memberHelper.getAllMembersInfo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getAllMembersInfo).toHaveBeenCalledWith(authorId, authorFull);
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[[mockMember.name, 'remove'], null, null, 'remove'],
|
||||||
|
[[mockMember.name, 'remove', 'test'], null, null, 'remove'],
|
||||||
|
[[mockMember.name, 'new'], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, null], mockMember.propic, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, null], mockMember.propic, attachmentExpiration, 'new'],
|
||||||
|
[[mockMember.name, 'name', mockMember.name], null, null, 'name'],
|
||||||
|
[[mockMember.name, 'displayname', mockMember.displayname], null, null, 'displayname'],
|
||||||
|
[[mockMember.name, 'new', mockMember.displayname], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', '', mockMember.proxy], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', '', '', mockMember.propic], null, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', '', '', undefined], mockMember.propic, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', '', '', undefined], mockMember.propic, attachmentExpiration, 'new'],
|
||||||
|
[[mockMember.name, 'new', '', ''], mockMember.propic, null, 'new'],
|
||||||
|
[[mockMember.name, 'new', '', ''], mockMember.propic, attachmentExpiration, 'new'],
|
||||||
|
[[mockMember.name, 'proxy', mockMember.proxy], null, null, 'proxy'],
|
||||||
|
[[mockMember.name, 'propic', mockMember.propic], null, null, 'propic'],
|
||||||
|
[[mockMember.name, 'propic', undefined], mockMember.propic, null, 'propic'],
|
||||||
|
[[mockMember.name, 'propic', undefined], mockMember.propic, attachmentExpiration, 'propic'],
|
||||||
|
[[mockMember.name, 'propic'], mockMember.propic, null, 'propic'],
|
||||||
|
[[mockMember.name, 'propic'], mockMember.propic, attachmentExpiration, 'propic'],
|
||||||
|
[['remove', mockMember.name], null, null, 'remove'],
|
||||||
|
[['remove', mockMember.name, 'test'], null, null, 'remove'],
|
||||||
|
[['new', mockMember.name], null, null, 'new'],
|
||||||
|
[['new', mockMember.name, mockMember.displayname], null, null, 'new'],
|
||||||
|
[['new', mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'new'],
|
||||||
|
[['new', mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'new'],
|
||||||
|
[['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'new'],
|
||||||
|
[['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'new'],
|
||||||
|
[['new', mockMember.name, '', mockMember.proxy], null, null, 'new'],
|
||||||
|
[['new', mockMember.name, '', '', mockMember.propic], null, null, 'new'],
|
||||||
|
[['new', mockMember.name, '', '', undefined], mockMember.propic, null, 'new'],
|
||||||
|
[['new', mockMember.name, '', '', undefined], mockMember.propic, attachmentExpiration, 'new'],
|
||||||
|
[['new', mockMember.name, '', ''], mockMember.propic, null, 'new'],
|
||||||
|
[['new', mockMember.name, '', ''], mockMember.propic, attachmentExpiration, 'new'],
|
||||||
|
[['name', mockMember.name, mockMember.name], null, null, 'name'],
|
||||||
|
[['displayname', mockMember.name, mockMember.name, mockMember.displayname], null, null, 'displayname'],
|
||||||
|
[['proxy', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'proxy'],
|
||||||
|
[['propic', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'propic'],
|
||||||
|
[['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'propic'],
|
||||||
|
[['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'propic']
|
||||||
|
])('%s args with attachmentURL %s and attachment expiration %s calls memberCommandHandler', async (args, attachmentUrl, attachmentExpiration, command) => {
|
||||||
|
// Arrange
|
||||||
|
let values = args.slice(2);
|
||||||
|
|
||||||
|
const result = await memberHelper.memberArgumentHandler(authorId, authorFull, false, command, mockMember.name, args, attachmentUrl, attachmentExpiration);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("handled command");
|
||||||
|
expect(memberHelper.memberCommandHandler).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.memberCommandHandler).toHaveBeenCalledWith(authorId, command, mockMember.name, values, attachmentUrl, attachmentExpiration);
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[null],
|
||||||
|
['name'],
|
||||||
|
['displayname'],
|
||||||
|
['proxy'],
|
||||||
|
['propic'],
|
||||||
|
])('%s calls sendCurrentValue', async (command) => {
|
||||||
|
const result = await memberHelper.memberArgumentHandler(authorId, authorFull, false, command, mockMember.name, []);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("current value");
|
||||||
|
expect(memberHelper.sendCurrentValue).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.sendCurrentValue).toHaveBeenCalledWith(authorId, mockMember.name, command);
|
||||||
|
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendCurrentValue', () => {
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['name', `The name of ${mockMember.name} is \"${mockMember.name}\" but you probably already knew that!`],
|
||||||
|
['displayname', `The display name for ${mockMember.name} is \"${mockMember.displayname}\".`],
|
||||||
|
['proxy', `The proxy for ${mockMember.name} is \"${mockMember.proxy}\".`],
|
||||||
|
['propic', `The profile picture for ${mockMember.name} is \"${mockMember.propic}\".`],
|
||||||
|
])('%s calls getMemberByName and returns value', async (command, expected) => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue(mockMember);
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns error if no member found', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue(null);
|
||||||
|
// Act
|
||||||
|
await expect(memberHelper.sendCurrentValue(authorId, mockMember.name, 'name')).rejects.toThrow(enums.err.NO_MEMBER);
|
||||||
|
// Assert
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls getMemberInfo with member if no command present', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue(mockMember);
|
||||||
|
jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue('member info');
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, null);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual('member info');
|
||||||
|
expect(memberHelper.getMemberInfo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(mockMember);
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['displayname', `Display name ${enums.err.NO_VALUE}`],
|
||||||
|
['proxy', `Proxy ${enums.err.NO_VALUE}`],
|
||||||
|
['propic', `Propic ${enums.err.NO_VALUE}`],
|
||||||
|
])('returns null message if no value found', async (command, expected) => {
|
||||||
|
// Arrange
|
||||||
|
const empty = {name: mockMember.name, displayname: null, proxy: null, propic: null}
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue(empty);
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addNewMember', () => {
|
||||||
|
test('calls addFullMember with correct arguments', async () => {
|
||||||
|
// Arrange
|
||||||
|
const args = [mockMember.displayname, mockMember.proxy, mockMember.propic];
|
||||||
|
jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(mockMember);
|
||||||
|
jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue();
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.addNewMember(authorId, mockMember.name, args, attachmentUrl, attachmentExpiration);
|
||||||
|
// Assert
|
||||||
|
expect(memberHelper.addFullMember).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.addFullMember).toHaveBeenCalledWith(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic, attachmentExpiration);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls getMemberInfo when successful and returns result', async () => {
|
||||||
|
// Arrange
|
||||||
|
const args = [mockMember.displayname, mockMember.proxy, mockMember.propic];
|
||||||
|
const fullMemberResponse = {member: mockMember, errors: []}
|
||||||
|
const expected = {
|
||||||
|
embed: mockMember,
|
||||||
|
errors: [],
|
||||||
|
success: `${mockMember.name} has been added successfully.`
|
||||||
|
};
|
||||||
|
jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(fullMemberResponse);
|
||||||
|
jest.spyOn(memberHelper, 'getMemberInfo').mockReturnValue(mockMember);
|
||||||
|
//Act
|
||||||
|
const result = await memberHelper.addNewMember(authorId, mockMember.name, args, attachmentUrl, attachmentExpiration);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(memberHelper.getMemberInfo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(mockMember);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws expected error when getMemberInfo throws error', async () => {
|
||||||
|
// Arrange
|
||||||
|
const args = [];
|
||||||
|
const memberObject = {name: args[1]}
|
||||||
|
jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject);
|
||||||
|
jest.spyOn(memberHelper, 'getMemberInfo').mockImplementation(() => {throw new Error('getMemberInfo error')});
|
||||||
|
//Act
|
||||||
|
await expect(memberHelper.addNewMember(authorId, mockMember.name, args)).rejects.toThrow('getMemberInfo error');
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws expected error when addFullMember throws error', async () => {
|
||||||
|
// Arrange
|
||||||
|
const args = [];
|
||||||
|
const expected = 'add full member error';
|
||||||
|
jest.spyOn(memberHelper, 'addFullMember').mockRejectedValue(new Error(expected));
|
||||||
|
|
||||||
|
//Act
|
||||||
|
await expect(memberHelper.addNewMember(authorId, mockMember.name, args)).rejects.toThrow(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateName', () => {
|
||||||
|
|
||||||
|
test('call updateMemberField with correct arguments when displayname passed in correctly and returns string', async () => {
|
||||||
|
// Arrange;
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.updateName(authorId, mockMember.name, " somePerson ")
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("Updated");
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "name", "somePerson");
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws error when name is blank', async () => {
|
||||||
|
// Arrange;
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
// Act & Assert
|
||||||
|
await expect(memberHelper.updateName(authorId, mockMember.name, " ")).rejects.toThrow('Name ' + enums.err.NO_VALUE);
|
||||||
|
// Assert
|
||||||
|
expect(memberHelper.updateMemberField).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateDisplayName', () => {
|
||||||
|
|
||||||
|
test('throws error when displayname is blank', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue();
|
||||||
|
// Act & Assert
|
||||||
|
await expect(memberHelper.updateDisplayName(authorId, mockMember.name, " ")).rejects.toThrow("Display name " + enums.err.NO_VALUE);
|
||||||
|
// Assert
|
||||||
|
expect(memberHelper.updateMemberField).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Sends error when display name is too long', async () => {
|
||||||
|
// Arrange
|
||||||
|
const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue();
|
||||||
|
// Act & Assert
|
||||||
|
await expect(memberHelper.updateDisplayName(authorId, mockMember.name, tooLongDisplayName)).rejects.toThrow(enums.err.DISPLAY_NAME_TOO_LONG);
|
||||||
|
// Assert
|
||||||
|
expect(memberHelper.updateMemberField).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
test('call updateMemberField with correct arguments when displayname passed in correctly and returns string', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.updateDisplayName(authorId, mockMember.name, " Some Person ");
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("Updated");
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "displayname", mockMember.displayname);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateProxy', () => {
|
||||||
|
test('calls checkIfProxyExists and updateMemberField and returns string', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue();
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.updateProxy(authorId, mockMember.name, "--text");
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("Updated");
|
||||||
|
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "proxy", mockMember.proxy);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updatePropic', () => {
|
||||||
|
test.each([
|
||||||
|
[null, attachmentUrl, undefined, attachmentUrl],
|
||||||
|
[mockMember.propic, null, undefined, mockMember.propic],
|
||||||
|
[mockMember.propic, attachmentUrl, undefined, mockMember.propic],
|
||||||
|
])('calls checkImageFormatValidity and updateMemberField and returns string', async (imgUrl, attachmentUrl, attachmentExpiration, expected) => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
utils.setExpirationWarning = jest.fn().mockReturnValue(undefined);
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.updatePropic(authorId, mockMember.name, imgUrl, attachmentUrl, attachmentExpiration);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("Updated");
|
||||||
|
expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1);
|
||||||
|
expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(expected);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "propic", expected, undefined);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls setExpirationWarning', async() => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
utils.setExpirationWarning = jest.fn().mockReturnValue(enums.misc.ATTACHMENT_EXPIRATION_WARNING);
|
||||||
|
// Act
|
||||||
|
const result = await memberHelper.updatePropic(authorId, mockMember.name, null, attachmentUrl, attachmentExpiration);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("Updated");
|
||||||
|
expect(utils.setExpirationWarning).toHaveBeenCalledTimes(1);
|
||||||
|
expect(utils.setExpirationWarning).toHaveBeenCalledWith(attachmentUrl, attachmentExpiration);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "propic", attachmentUrl, enums.misc.ATTACHMENT_EXPIRATION_WARNING);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addFullMember', () => {
|
||||||
|
test('calls getMemberByName', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue();
|
||||||
|
// Act
|
||||||
|
await memberHelper.addFullMember(authorId, mockMember.name)
|
||||||
|
// Assert
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
|
||||||
|
expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if getMemberByName returns member, throw error', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue({name: mockMember.name});
|
||||||
|
// Act & Assert
|
||||||
|
await expect(memberHelper.addFullMember(authorId, mockMember.name)).rejects.toThrow(`Can't add ${mockMember.name}. ${enums.err.MEMBER_EXISTS}`)
|
||||||
|
// Assert
|
||||||
|
expect(memberRepo.createMember).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('if name is not filled out, throw error', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue();
|
||||||
|
// Act
|
||||||
|
await expect(memberHelper.addFullMember(authorId, " ")).rejects.toThrow(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`);
|
||||||
|
// Assert
|
||||||
|
expect(memberRepo.createMember).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if displayname is over 32 characters, call memberRepo.createMember with null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.getMemberByName.mockResolvedValue();
|
||||||
|
const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {
|
||||||
|
member: expectedMemberArgs,
|
||||||
|
errors: [`Tried to set displayname to \"${tooLongDisplayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.addFullMember(authorId, mockMember.name, tooLongDisplayName, null, null);
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if proxy, call checkIfProxyExists', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(true);
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {member: expectedMemberArgs, errors: []}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy)
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy);
|
||||||
|
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if checkProxyExists throws error, call database.member.create with null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockRejectedValue(new Error('error'));
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {
|
||||||
|
member: expectedMemberArgs,
|
||||||
|
errors: [`Tried to set proxy to \"${mockMember.proxy}\". error. ${enums.err.SET_TO_NULL}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy, null)
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if propic, call checkImageFormatValidity', async () => {
|
||||||
|
// Arrange
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
utils.setExpirationWarning = jest.fn().mockReturnValue();
|
||||||
|
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {member: expectedMemberArgs, errors: []}
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic);
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(mockMember.propic);
|
||||||
|
expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if checkImageFormatValidity throws error, call database.member.create with null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
utils.checkImageFormatValidity = jest.fn().mockRejectedValue(new Error("error"));
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {
|
||||||
|
member: expectedMemberArgs,
|
||||||
|
errors: [`Tried to set profile picture to \"${mockMember.propic}\". error. ${enums.err.SET_TO_NULL}`]
|
||||||
|
}
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic);
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls setExpirationWarning if attachmentExpiration exists', async () => {
|
||||||
|
// Arrange
|
||||||
|
utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true);
|
||||||
|
utils.setExpirationWarning = jest.fn().mockReturnValue(`${enums.misc.ATTACHMENT_EXPIRATION_WARNING}`);
|
||||||
|
// Act
|
||||||
|
await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic, attachmentExpiration)
|
||||||
|
// Assert
|
||||||
|
expect(utils.setExpirationWarning).toHaveBeenCalledTimes(1);
|
||||||
|
expect(utils.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic, attachmentExpiration);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if all values are valid, call database.members.create', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(false);
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: mockMember.displayname,
|
||||||
|
proxy: mockMember.proxy,
|
||||||
|
propic: mockMember.propic
|
||||||
|
}
|
||||||
|
memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true);
|
||||||
|
utils.setExpirationWarning = jest.fn().mockReturnValue();
|
||||||
|
const expectedReturn = {member: expectedMemberArgs, errors: []}
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.addFullMember(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic);
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateMemberField', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
utils.setExpirationWarning = jest.fn().mockReturnValue(`warning`);
|
||||||
|
memberRepo.updateMemberField = jest.fn().mockResolvedValue([1]);
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['name', mockMember.name, undefined, `Updated name for ${mockMember.name} to ${mockMember.name}.`],
|
||||||
|
['displayname', mockMember.displayname, undefined, `Updated displayname for ${mockMember.name} to ${mockMember.displayname}.`],
|
||||||
|
['proxy', mockMember.proxy, undefined, `Updated proxy for ${mockMember.name} to ${mockMember.proxy}.`],
|
||||||
|
['propic', mockMember.propic, undefined, `Updated propic for ${mockMember.name} to ${mockMember.propic}.`],
|
||||||
|
['propic', mockMember.propic,
|
||||||
|
'warning', `Updated propic for ${mockMember.name} to ${mockMember.propic}. warning.`]
|
||||||
|
])('calls database.members.update with correct column and value and return string', async (columnName, value, attachmentExpiration, expected) => {
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration)
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expected);
|
||||||
|
expect(memberRepo.updateMemberField).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, columnName, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if database.members.update returns 0 rows changed, throw error', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberRepo.updateMemberField = jest.fn().mockResolvedValue(0);
|
||||||
|
// Act
|
||||||
|
await expect(memberHelper.updateMemberField(authorId, mockMember.name, "displayname", mockMember.displayname)).rejects.toThrow(`Can't update ${mockMember.name}. ${enums.err.NO_MEMBER}.`);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkIfProxyExists', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
memberRepo.getMembersByAuthor.mockResolvedValue([mockMember]);
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['!text'],
|
||||||
|
['! text'],
|
||||||
|
['⭐text'],
|
||||||
|
['⭐ text'],
|
||||||
|
['⭐ text ⭐'],
|
||||||
|
['--text--'],
|
||||||
|
['!text ?'],
|
||||||
|
['SP: text'],
|
||||||
|
['text --SP'],
|
||||||
|
])('%s should call getMembersByAuthor and return false', async (proxy) => {
|
||||||
|
// Act
|
||||||
|
const res = await memberHelper.checkIfProxyExists(authorId, proxy)
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(false)
|
||||||
|
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledWith(authorId);
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['--', enums.err.NO_TEXT_FOR_PROXY, false],
|
||||||
|
[' ', enums.err.NO_TEXT_FOR_PROXY, false],
|
||||||
|
['text', enums.err.NO_PROXY_WRAPPER, false],
|
||||||
|
])('%s returns correct error and does not call getMemberByAuthor', async (proxy, error, shouldCall) => {
|
||||||
|
// Act & Assert
|
||||||
|
await expect(memberHelper.checkIfProxyExists(authorId, proxy)).rejects.toThrow(error);
|
||||||
|
|
||||||
|
expect(memberRepo.getMembersByAuthor).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--text returns correct error and calls getMemberByAuthor', async () => {
|
||||||
|
await expect(memberHelper.checkIfProxyExists(authorId, "--text")).rejects.toThrow(enums.err.PROXY_EXISTS);
|
||||||
|
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberRepo.getMembersByAuthor).toHaveBeenCalledWith(authorId);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
121
tests/helpers/messageHelper.test.js
Normal file
121
tests/helpers/messageHelper.test.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const env = require('dotenv');
|
||||||
|
env.config();
|
||||||
|
|
||||||
|
|
||||||
|
jest.mock('../../src/repositories/memberRepo.js', () => {
|
||||||
|
return {
|
||||||
|
memberRepo: {
|
||||||
|
getMembersByAuthor: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const {messageHelper} = require("../../src/helpers/messageHelper.js");
|
||||||
|
const {memberRepo} = require("../../src/repositories/memberRepo");
|
||||||
|
|
||||||
|
describe('messageHelper', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseCommandArgs', () => {
|
||||||
|
test.each([
|
||||||
|
['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";
|
||||||
|
const result = messageHelper.parseCommandArgs(content, command);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`parseProxyTags`, () => {
|
||||||
|
const membersFor1 = [
|
||||||
|
{name: "somePerson", proxy: "--text"},
|
||||||
|
{name: "someSecondPerson", proxy: undefined},
|
||||||
|
{name: "someOtherPerson", proxy: "?text}"},
|
||||||
|
{name: "someLastPerson", proxy: "{text}"},
|
||||||
|
{name: "someEmojiPerson", proxy: "⭐text"},
|
||||||
|
{name: "someSpacePerson", proxy: "! text"},
|
||||||
|
]
|
||||||
|
|
||||||
|
const membersFor2 = []
|
||||||
|
|
||||||
|
const membersFor3 = [
|
||||||
|
{name: "someOtherThirdPerson", proxy: undefined}
|
||||||
|
]
|
||||||
|
|
||||||
|
const attachmentUrl = "../oya.png"
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
memberRepo.getMembersByAuthor = jest.fn().mockImplementation((specificAuthorId) => {
|
||||||
|
if (specificAuthorId === "1") return membersFor1;
|
||||||
|
if (specificAuthorId === "2") return membersFor2;
|
||||||
|
if (specificAuthorId === "3") return membersFor3;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['1', 'hello', null, {}],
|
||||||
|
['1', '--hello', null, {member: membersFor1[0], message: 'hello', hasAttachment: false}],
|
||||||
|
['1', 'hello', attachmentUrl, {}],
|
||||||
|
['1', '--hello', attachmentUrl, {member: membersFor1[0], message: 'hello', hasAttachment: true}],
|
||||||
|
['1', '--', attachmentUrl, {member: membersFor1[0], message: '', hasAttachment: true}],
|
||||||
|
['1', '?hello}', null, {member: membersFor1[2], message: 'hello', hasAttachment: false}],
|
||||||
|
['1', '{hello}', null, {member: membersFor1[3], message: 'hello', hasAttachment: false}],
|
||||||
|
['1', '⭐hello', null, {member: membersFor1[4], message: 'hello', hasAttachment: false}],
|
||||||
|
['1', '! hello', null, {member: membersFor1[5], message: 'hello', hasAttachment: false}],
|
||||||
|
['2', 'hello', null, undefined],
|
||||||
|
['2', '--hello', null, 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) => {
|
||||||
|
// Act
|
||||||
|
const res = await messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl);
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expected);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('returnBufferFromText', () => {
|
||||||
|
const charas2000 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||||
|
|
||||||
|
test('returns truncated text and buffer file when text is more than 2000 characters', () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
const charasOver2000 = "bbbbb"
|
||||||
|
const expectedBuffer = Buffer.from(charasOver2000, 'utf-8');
|
||||||
|
const expected = {text: charas2000, file: expectedBuffer};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = messageHelper.returnBufferFromText(`${charas2000}${charasOver2000}`);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns text when text is 2000 characters or less', () => {
|
||||||
|
// Arrange
|
||||||
|
const expected = {text: charas2000, file: undefined};
|
||||||
|
// Act
|
||||||
|
const result = messageHelper.returnBufferFromText(`${charas2000}`);
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
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();
|
||||||
|
});
|
||||||
|
})
|
||||||
306
tests/helpers/webhookHelper.test.js
Normal file
306
tests/helpers/webhookHelper.test.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
jest.mock('../../src/helpers/messageHelper.js')
|
||||||
|
|
||||||
|
const {messageHelper} = require("../../src/helpers/messageHelper.js");
|
||||||
|
|
||||||
|
jest.mock('../../src/helpers/messageHelper.js', () => {
|
||||||
|
return {messageHelper: {
|
||||||
|
parseProxyTags: jest.fn(),
|
||||||
|
returnBuffer: jest.fn(),
|
||||||
|
returnBufferFromText: jest.fn(),
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
|
||||||
|
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`, () => {
|
||||||
|
const client = {};
|
||||||
|
const content = "hi"
|
||||||
|
const attachments = {
|
||||||
|
size: 0,
|
||||||
|
first: () => {}
|
||||||
|
}
|
||||||
|
const message = {
|
||||||
|
client,
|
||||||
|
content: content,
|
||||||
|
attachments: attachments,
|
||||||
|
author: {
|
||||||
|
id: '123'
|
||||||
|
},
|
||||||
|
guildId: '123',
|
||||||
|
reply: jest.fn()
|
||||||
|
}
|
||||||
|
const member = {proxy: "--text", name: 'somePerson', displayname: "Some Person"};
|
||||||
|
const proxyMessage = {message: content, member: member}
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(webhookHelper, 'replaceMessage');
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls parseProxyTags and returns if proxyMatch is empty object', async() => {
|
||||||
|
// Arrange
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue({});
|
||||||
|
// Act
|
||||||
|
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
|
||||||
|
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() => {
|
||||||
|
// Arrange
|
||||||
|
message.attachments = {
|
||||||
|
size: 1,
|
||||||
|
first: () => {
|
||||||
|
return {url: 'oya.png'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(undefined);
|
||||||
|
// Act
|
||||||
|
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 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() => {
|
||||||
|
// Arrange
|
||||||
|
message.guildId = '123'
|
||||||
|
proxyMessage.hasAttachment = true;
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
|
||||||
|
const expected = `${enums.misc.ATTACHMENT_SENT_BY} ${proxyMessage.member.displayname}`
|
||||||
|
// Act
|
||||||
|
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() => {
|
||||||
|
// Arrange
|
||||||
|
message.guildId = '123';
|
||||||
|
proxyMessage.hasAttachment = false;
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
|
||||||
|
jest.spyOn(webhookHelper, 'replaceMessage').mockResolvedValue();
|
||||||
|
// Act
|
||||||
|
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 and does not call message.reply', async () => {
|
||||||
|
// Arrange
|
||||||
|
message.guildId = '123';
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
|
||||||
|
jest.spyOn(webhookHelper, 'replaceMessage').mockRejectedValue(new Error("error"));
|
||||||
|
// Act
|
||||||
|
await expect(webhookHelper.sendMessageAsMember(client, message)).rejects.toThrow("error");
|
||||||
|
// Assert
|
||||||
|
expect(message.reply).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`replaceMessage`, () => {
|
||||||
|
|
||||||
|
test('does not call anything if text is 0 or message has no attachments', async() => {
|
||||||
|
// Arrange
|
||||||
|
const emptyText = ''
|
||||||
|
const noAttachments = {
|
||||||
|
size: 0,
|
||||||
|
first: () => {}
|
||||||
|
}
|
||||||
|
message.attachments = noAttachments;
|
||||||
|
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
|
||||||
|
// Act
|
||||||
|
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 & < 2000', async() => {
|
||||||
|
// Arrange
|
||||||
|
message.attachments = {
|
||||||
|
size: 0,
|
||||||
|
first: () => {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
|
||||||
|
// Act
|
||||||
|
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. 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
|
||||||
|
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 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: '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();
|
||||||
|
messageHelper.returnBufferFromText = jest.fn().mockReturnValue(returnedBuffer);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
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