forked from PluralFlux/PluralFlux
Compare commits
128 Commits
more-disco
...
bf4f55c91b
| Author | SHA1 | Date | |
|---|---|---|---|
| bf4f55c91b | |||
| b3566010a2 | |||
| 0eee2988ce | |||
| a86260cc4a | |||
| 506e3ef9dd | |||
| 6a33fb592a | |||
| 2f255cefd1 | |||
| c54016de77 | |||
| 31da15eaeb | |||
| 43f4302dbc | |||
| af3da44946 | |||
| af57e2e6a3 | |||
| 77191566e3 | |||
| 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
|
||||||
45
.gitea/workflows/build-dev.yml
Normal file
45
.gitea/workflows/build-dev.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: nodeJS remote worker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["develop"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["develop"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: login to gitea registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ gitea.server_url }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.GITEA }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
engineering.sanya.gay/pluralflux/pluralflux:latest
|
||||||
|
|
||||||
|
- name: Deploy bot
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
port: 22
|
||||||
|
script: |
|
||||||
|
cd /root/pluralflux-dev/PluralFlux
|
||||||
|
docker compose up -d
|
||||||
46
.gitea/workflows/build-main.yml
Normal file
46
.gitea/workflows/build-main.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: nodeJS remote worker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: login to gitea registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ gitea.server_url }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.GITEA }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
engineering.sanya.gay/pluralflux/pluralflux:latest
|
||||||
|
|
||||||
|
- name: Deploy bot
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
port: 22
|
||||||
|
script: |
|
||||||
|
cd /root/pluralflux-prod/PluralFlux
|
||||||
|
docker compose up -d
|
||||||
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
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.idea
|
.idea
|
||||||
secrets/
|
secrets/
|
||||||
package-lock.json
|
config.json
|
||||||
config.json
|
coverage
|
||||||
|
log.txt
|
||||||
|
.env
|
||||||
|
oya.png
|
||||||
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 ["node", "src/bot.js"]
|
||||||
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.
|
||||||
|
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
If it's not running at the moment, it's because my computer crashed or something. I'm looking to move running it to a somewhat more permanent solution.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
All commands are prefixed by `pf;`. Currently only a few are implemented.
|
||||||
|
|
||||||
|
- `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();
|
|
||||||
46
compose.yaml
46
compose.yaml
@@ -1,33 +1,35 @@
|
|||||||
services:
|
services:
|
||||||
|
main:
|
||||||
|
image: engineering.sanya.gay/pluralflux/pluralflux-dev
|
||||||
|
container_name: pluralflux
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- pluralflux-net
|
||||||
|
env_file: "secrets.env"
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
container_name: pluralflux-postgres
|
container_name: pluralflux-postgres
|
||||||
environment:
|
env_file: "secrets.env"
|
||||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pwd
|
|
||||||
secrets:
|
|
||||||
- postgres_pwd
|
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql
|
- pgdata:/var/lib/postgresql
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
# pgadmin:
|
networks:
|
||||||
# image: dpage/pgadmin4:latest
|
- pluralflux-net
|
||||||
# ports:
|
pgadmin:
|
||||||
# - 5050:80
|
image: dpage/pgadmin4:latest
|
||||||
# environment:
|
container_name: pluralflux-pgadmin
|
||||||
# # Required by pgAdmin
|
ports:
|
||||||
# PGADMIN_DEFAULT_EMAIL: pieartsy@pm.me
|
- "5050:80"
|
||||||
# PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/postgres_pwd
|
env_file: "secrets.env"
|
||||||
# # Don't require the user to login
|
depends_on:
|
||||||
# PGADMIN_CONFIG_SERVER_MODE: 'False'
|
- postgres
|
||||||
# # Don't require a "master" password after logging in
|
networks:
|
||||||
# PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
- pluralflux-net
|
||||||
# secrets:
|
|
||||||
# - postgres_pwd
|
networks:
|
||||||
|
pluralflux-net:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
secrets:
|
||||||
secrets:
|
|
||||||
postgres_pwd:
|
|
||||||
file: ./secrets/postgres-password.txt
|
|
||||||
|
|||||||
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')
|
||||||
|
},
|
||||||
|
};
|
||||||
7788
package-lock.json
generated
Normal file
7788
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -2,24 +2,32 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"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",
|
|
||||||
"canvas": "^3.2.1",
|
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
"sequelize": "^6.37.7"
|
"pm2": "^6.0.14",
|
||||||
|
"sequelize": "^6.37.7",
|
||||||
|
"tmp": "^0.2.5"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
"babel-jest": "^30.2.0",
|
||||||
|
"fetch-mock": "^12.6.0",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"jest-fetch-mock": "^3.0.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
secrets.env
Normal file
6
secrets.env
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FLUXER_BOT_TOKEN=<your bot token here>
|
||||||
|
POSTGRES_PASSWORD=<your postgres password here>
|
||||||
|
PGADMIN_DEFAULT_EMAIL: <default postgres admin login>
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: <your postgres password here>
|
||||||
|
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||||
|
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||||
100
src/bot.js
Normal file
100
src/bot.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Client, Events, Message } from '@fluxerjs/core';
|
||||||
|
import { messageHelper } from "./helpers/messageHelper.js";
|
||||||
|
import {enums} from "./enums.js";
|
||||||
|
import {commands} from "./commands.js";
|
||||||
|
import {webhookHelper} from "./helpers/webhookHelper.js";
|
||||||
|
import env from 'dotenv';
|
||||||
|
import {utils} from "./helpers/utils.js";
|
||||||
|
|
||||||
|
env.config({path: './.env'});
|
||||||
|
|
||||||
|
export const token = process.env.FLUXER_BOT_TOKEN;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error("Missing FLUXER_BOT_TOKEN environment variable.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const client = new Client({ intents: 0 });
|
||||||
|
|
||||||
|
client.on(Events.MessageCreate, async (message) => {
|
||||||
|
await handleMessageCreate(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls functions based off the contents of a message object.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {Message} message - The message object
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
export const handleMessageCreate = async function(message) {
|
||||||
|
try {
|
||||||
|
// Parse command and arguments
|
||||||
|
const content = message.content.trim();
|
||||||
|
// Ignore bots and messages without content
|
||||||
|
if (message.author.bot || content.length === 0) return;
|
||||||
|
|
||||||
|
// If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return.
|
||||||
|
if (!content.startsWith(messageHelper.prefix)) {
|
||||||
|
await webhookHelper.sendMessageAsMember(client, message).catch((e) => {
|
||||||
|
throw e
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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).catch(e => {
|
||||||
|
throw e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await message.reply(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
// return await message.reply(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
await client.login(token);
|
||||||
|
// await db.check_connection();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
110
src/commands.js
Normal file
110
src/commands.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import {messageHelper} from "./helpers/messageHelper.js";
|
||||||
|
import {enums} from "./enums.js";
|
||||||
|
import {memberHelper} from "./helpers/memberHelper.js";
|
||||||
|
import {EmbedBuilder} from "@fluxerjs/core";
|
||||||
|
import {importHelper} from "./helpers/importHelper.js";
|
||||||
|
|
||||||
|
const cmds = {
|
||||||
|
commandsMap: new Map(),
|
||||||
|
aliasesMap: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
cmds.aliasesMap.set('m', {command: 'member'})
|
||||||
|
|
||||||
|
cmds.commandsMap.set('member', {
|
||||||
|
description: enums.help.SHORT_DESC_MEMBER,
|
||||||
|
async execute(message, args) {
|
||||||
|
await cmds.memberCommand(message, args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the member-related functions.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {Message} message - The message object
|
||||||
|
* @param {string[]} args - The parsed arguments
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
cmds.memberCommand = async function(message, args) {
|
||||||
|
const authorFull = `${message.author.username}#${message.author.discriminator}`
|
||||||
|
const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null;
|
||||||
|
const attachmentExpires = message.attachments.size > 0 ? message.attachments.first().expires_at : null;
|
||||||
|
|
||||||
|
const reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires).catch(async (e) =>{console.error(e); 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') {
|
||||||
|
const errorsText = reply.errors.length > 0 ? reply.errors.join('\n- ') : null;
|
||||||
|
return await message.reply({content: `${reply.success} ${errorsText ? `\n\n${enums.err.ERRORS_OCCURRED}\n` + errorsText : ""}`, embeds: [reply.embed]})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cmds.commandsMap.set('help', {
|
||||||
|
description: enums.help.SHORT_DESC_HELP,
|
||||||
|
async execute(message) {
|
||||||
|
const fields = [...cmds.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] });
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmds.commandsMap.set('import', {
|
||||||
|
description: enums.help.SHORT_DESC_IMPORT,
|
||||||
|
async execute(message, args) {
|
||||||
|
await cmds.importCommand(message, args);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the import-related functions.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {Message} message - The message object
|
||||||
|
* @param {string[]} args - The parsed arguments
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
cmds.importCommand = async function(message, args) {
|
||||||
|
const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null;
|
||||||
|
if ((message.content.includes('--help') || (args[0] === '' && args.length === 1)) && !attachmentUrl ) {
|
||||||
|
return await message.reply(enums.help.IMPORT);
|
||||||
|
}
|
||||||
|
return await importHelper.pluralKitImport(message.author.id, attachmentUrl).then(async (successfullyAdded) => {
|
||||||
|
await message.reply(successfullyAdded);
|
||||||
|
}).catch(async (error) => {
|
||||||
|
if (error instanceof AggregateError) {
|
||||||
|
// errors.message can be a list of successfully added members, or say that none were successful.
|
||||||
|
let errorsText = `${error.message}.\n\n${enums.err.ERRORS_OCCURRED}\n${error.errors.join('\n')}`;
|
||||||
|
|
||||||
|
await message.reply(errorsText).catch(async () => {
|
||||||
|
const returnedBuffer = messageHelper.returnBufferFromText(errorsText);
|
||||||
|
await message.reply({content: returnedBuffer.text, files: [{ name: 'text.txt', data: returnedBuffer.file }]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If just one error was returned.
|
||||||
|
else {
|
||||||
|
return await message.reply(error.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commands = cmds;
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import {DataTypes, Sequelize} from 'sequelize';
|
import {DataTypes, Sequelize} from 'sequelize';
|
||||||
|
import * as env from 'dotenv';
|
||||||
|
|
||||||
|
env.config();
|
||||||
|
|
||||||
const password = process.env.POSTGRES_PASSWORD;
|
const password = process.env.POSTGRES_PASSWORD;
|
||||||
|
|
||||||
@@ -7,17 +10,18 @@ if (!password) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = {};
|
const db = {};
|
||||||
|
|
||||||
const sequelize = new Sequelize('postgres', 'postgres', password, {
|
const sequelize = new Sequelize('postgres', 'postgres', password, {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
logging: false,
|
||||||
dialect: 'postgres'
|
dialect: 'postgres'
|
||||||
});
|
});
|
||||||
|
|
||||||
database.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
database.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
database.members = sequelize.define('Member', {
|
db.members = sequelize.define('Member', {
|
||||||
userid: {
|
userid: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
@@ -37,8 +41,26 @@ database.members = sequelize.define('Member', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
database.check_connection = async function() {
|
db.systems = sequelize.define('System', {
|
||||||
await sequelize.authenticate().then(async (result) => {
|
userid: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
fronter: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
grouptag: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
autoproxy: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks Sequelize database connection.
|
||||||
|
*/
|
||||||
|
db.check_connection = async function() {
|
||||||
|
await sequelize.authenticate().then(async () => {
|
||||||
console.log('Connection has been established successfully.');
|
console.log('Connection has been established successfully.');
|
||||||
await syncModels();
|
await syncModels();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
@@ -47,8 +69,11 @@ database.check_connection = async function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs Sequelize models.
|
||||||
|
*/
|
||||||
async function syncModels() {
|
async function syncModels() {
|
||||||
await sequelize.sync().then((result) => {
|
await sequelize.sync().then(() => {
|
||||||
console.log('Models synced successfully.');
|
console.log('Models synced successfully.');
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('Syncing models did not work', err);
|
console.error('Syncing models did not work', err);
|
||||||
@@ -56,4 +81,4 @@ async function syncModels() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = database;
|
export const database = db;
|
||||||
49
src/enums.js
Normal file
49
src/enums.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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.",
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
helperEnums.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."
|
||||||
|
}
|
||||||
|
|
||||||
|
helperEnums.misc = {
|
||||||
|
ATTACHMENT_SENT_BY: "Attachment sent by:"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enums = helperEnums;
|
||||||
43
src/helpers/importHelper.js
Normal file
43
src/helpers/importHelper.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {enums} from "../enums.js";
|
||||||
|
import {memberHelper} from "./memberHelper.js";
|
||||||
|
|
||||||
|
const ih = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to import from Pluralkit.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @param {string | null} [attachmentUrl] - The attached JSON url.
|
||||||
|
* @returns {string} A successful addition of all members.
|
||||||
|
* @throws {Error} When the member exists, or creating a member doesn't work.
|
||||||
|
*/
|
||||||
|
ih.pluralKitImport = async function (authorId, attachmentUrl= null) {
|
||||||
|
if (!attachmentUrl) {
|
||||||
|
throw new Error(enums.err.NOT_JSON_FILE);
|
||||||
|
}
|
||||||
|
return fetch(attachmentUrl).then((res) => res.json()).then(async(pkData) => {
|
||||||
|
const pkMembers = pkData.members;
|
||||||
|
let errors = [];
|
||||||
|
const addedMembers = [];
|
||||||
|
for (let pkMember of pkMembers) {
|
||||||
|
const proxy = pkMember.proxy_tags[0] ? `${pkMember.proxy_tags[0].prefix ?? ''}text${pkMember.proxy_tags[0].suffix ?? ''}` : null;
|
||||||
|
await memberHelper.addFullMember(authorId, pkMember.name, pkMember.display_name, proxy, pkMember.avatar_url).then((memberObj) => {
|
||||||
|
addedMembers.push(memberObj.member.name);
|
||||||
|
if (memberObj.errors.length > 0) {
|
||||||
|
errors.push(`\n**${pkMember.name}:** `)
|
||||||
|
errors = errors.concat(memberObj.errors);
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
errors.push(e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : `${enums.err.NO_MEMBERS_IMPORTED}`;
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new AggregateError(errors, aggregatedText);
|
||||||
|
}
|
||||||
|
return aggregatedText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importHelper = ih;
|
||||||
553
src/helpers/memberHelper.js
Normal file
553
src/helpers/memberHelper.js
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
import {database} from '../database.js';
|
||||||
|
import {enums} from "../enums.js";
|
||||||
|
import {Op} from "sequelize";
|
||||||
|
import {EmbedBuilder} from "@fluxerjs/core";
|
||||||
|
import {utils} from "./utils.js";
|
||||||
|
|
||||||
|
const mh = {};
|
||||||
|
|
||||||
|
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.
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
mh.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 mh.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 mh.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses through the command, argument, and values and calls appropriate functions based on their presence or absence.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The id of the message author
|
||||||
|
* @param {string} authorFull - The username and discriminator of the message author
|
||||||
|
* @param {boolean} isHelp - Whether this is a help command or not
|
||||||
|
* @param {string | null} [command] - The command name
|
||||||
|
* @param {string | null} [memberName] - The member name
|
||||||
|
* @param {string[]} [args] - The message arguments
|
||||||
|
* @param {string | null} [attachmentUrl] - The attachment URL, if any
|
||||||
|
* @param {string | null} [attachmentExpiration] - The attachment expiry date, if any
|
||||||
|
* @returns {Promise<string>} A success message.
|
||||||
|
* @returns {Promise <EmbedBuilder>} A list of 25 members as an embed.
|
||||||
|
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
|
||||||
|
* @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors.
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
mh.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) {
|
||||||
|
if (!command && !memberName && !isHelp) {
|
||||||
|
throw new Error(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||||
|
}
|
||||||
|
else if (isHelp) {
|
||||||
|
return mh.sendHelpEnum(command);
|
||||||
|
}
|
||||||
|
else if (command === "list") {
|
||||||
|
return await mh.getAllMembersInfo(authorId, authorFull);
|
||||||
|
}
|
||||||
|
else if (!memberName && !isHelp) {
|
||||||
|
throw new Error(enums.err.NO_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove memberName and command from values to reduce confusion
|
||||||
|
const values = args.slice(2);
|
||||||
|
|
||||||
|
// ex: pf;member blah blah
|
||||||
|
if (command && memberName && (values.length > 0 || newAndRemoveCommands.includes(command) || attachmentUrl)) {
|
||||||
|
return await mh.memberCommandHandler(authorId, command, memberName, values, attachmentUrl, attachmentExpiration).catch((e) => {throw e});
|
||||||
|
}
|
||||||
|
else if (memberName && values.length === 0) {
|
||||||
|
return await mh.sendCurrentValue(authorId, memberName, command).catch((e) => {throw e});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
mh.sendCurrentValue = async function(authorId, memberName, command= null) {
|
||||||
|
const member = await mh.getMemberByName(authorId, memberName).then((m) => {
|
||||||
|
if (!m) throw new Error(enums.err.NO_MEMBER);
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
return mh.getMemberInfo(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'name':
|
||||||
|
return `The name of ${member.name} is \"${member.name}\" but you probably already knew that!`;
|
||||||
|
case 'displayname':
|
||||||
|
return member.displayname ? `The display name for ${member.name} is \"${member.displayname}\".` : `Display name ${enums.err.NO_VALUE}`;
|
||||||
|
case 'proxy':
|
||||||
|
return member.proxy ? `The proxy for ${member.name} is \"${member.proxy}\".` : `Proxy ${enums.err.NO_VALUE}`;
|
||||||
|
case 'propic':
|
||||||
|
return member.propic ? `The profile picture for ${member.name} is \"${member.propic}\".` : `Propic ${enums.err.NO_VALUE}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the help text associated with a command.
|
||||||
|
*
|
||||||
|
* @param {string} command - The command being called.
|
||||||
|
* @returns {string} - The help text associated with a command.
|
||||||
|
*/
|
||||||
|
mh.sendHelpEnum = function(command) {
|
||||||
|
switch (command) {
|
||||||
|
case 'new':
|
||||||
|
return 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>} 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}
|
||||||
|
*/
|
||||||
|
mh.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
|
||||||
|
switch (command) {
|
||||||
|
case 'new':
|
||||||
|
return await mh.addNewMember(authorId, memberName, values, attachmentUrl, attachmentExpiration).catch((e) => {throw e});
|
||||||
|
case 'remove':
|
||||||
|
return await mh.removeMember(authorId, memberName).catch((e) => {throw e});
|
||||||
|
case 'name':
|
||||||
|
return await mh.updateName(authorId, memberName, values[0]).catch((e) => {throw e});
|
||||||
|
case 'displayname':
|
||||||
|
return await mh.updateDisplayName(authorId, memberName, values[0]).catch((e) => {throw e});
|
||||||
|
case 'proxy':
|
||||||
|
return await mh.updateProxy(authorId, memberName, values[0]).catch((e) => {throw e});
|
||||||
|
case 'propic':
|
||||||
|
return await mh.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration).catch((e) => {throw e});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @throws {Error} When creating a member doesn't work.
|
||||||
|
*/
|
||||||
|
mh.addNewMember = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
|
||||||
|
const displayName = values[0];
|
||||||
|
const proxy = values[1];
|
||||||
|
const propic = values[2] ?? attachmentUrl;
|
||||||
|
|
||||||
|
return await mh.addFullMember(authorId, memberName, displayName, proxy, propic, attachmentExpiration).then((response) => {
|
||||||
|
const memberInfoEmbed = mh.getMemberInfo(response.member);
|
||||||
|
return {embed: memberInfoEmbed, errors: response.errors, success: `${memberName} has been added successfully.`};
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
mh.updateName = async function (authorId, memberName, name) {
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
if (trimmedName === '') {
|
||||||
|
throw new RangeError(`Name ${enums.err.NO_VALUE}`);
|
||||||
|
}
|
||||||
|
return await mh.updateMemberField(authorId, memberName, "name", trimmedName).catch((e) => {
|
||||||
|
throw e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
mh.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 mh.updateMemberField(authorId, membername, "displayname", trimmedName).catch((e) => {
|
||||||
|
throw e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @throws {Error} When an empty proxy was provided, or a proxy exists.
|
||||||
|
*/
|
||||||
|
mh.updateProxy = async function (authorId, memberName, proxy) {
|
||||||
|
// Throws error if exists
|
||||||
|
await mh.checkIfProxyExists(authorId, proxy).catch((e) => { throw e; });
|
||||||
|
|
||||||
|
return await mh.updateMemberField(authorId, memberName, "proxy", proxy).catch((e) => { throw e;});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @throws {Error} When loading the profile picture from a URL doesn't work.
|
||||||
|
*/
|
||||||
|
mh.updatePropic = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
|
||||||
|
const imgUrl = values ?? attachmentUrl;
|
||||||
|
// Throws error if invalid
|
||||||
|
await utils.checkImageFormatValidity(imgUrl).catch((e) => { throw e });
|
||||||
|
|
||||||
|
return await mh.updateMemberField(authorId, memberName, "propic", imgUrl, attachmentExpiration).catch((e) => { throw e });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
mh.removeMember = async function (authorId, memberName) {
|
||||||
|
return await database.members.destroy({
|
||||||
|
where: {
|
||||||
|
name: {[Op.iLike]: memberName},
|
||||||
|
userid: authorId
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result) {
|
||||||
|
return `Member "${memberName}" has been deleted.`;
|
||||||
|
}
|
||||||
|
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<{model, string[]}>} A successful addition object, including errors if there are any.
|
||||||
|
* @throws {Error} When the member already exists, there are validation errors, or adding a member doesn't work.
|
||||||
|
*/
|
||||||
|
mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) {
|
||||||
|
await mh.getMemberByName(authorId, memberName).then((member) => {
|
||||||
|
if (member) {
|
||||||
|
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) {
|
||||||
|
await mh.checkIfProxyExists(authorId, proxy).then(() => {
|
||||||
|
isValidProxy = true;
|
||||||
|
}).catch((e) => {
|
||||||
|
errors.push(`Tried to set proxy to \"${proxy}\". ${e.message}. ${enums.err.SET_TO_NULL}`);
|
||||||
|
isValidProxy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValidPropic;
|
||||||
|
if (propic && propic.length > 0) {
|
||||||
|
await utils.checkImageFormatValidity(propic).then(() => {
|
||||||
|
isValidPropic = true;
|
||||||
|
}).catch((e) => {
|
||||||
|
errors.push(`Tried to set profile picture to \"${propic}\". ${e.message}. ${enums.err.SET_TO_NULL}`);
|
||||||
|
isValidPropic = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isValidPropic && attachmentExpiration) {
|
||||||
|
errors.push(mh.setExpirationWarning(attachmentExpiration));
|
||||||
|
}
|
||||||
|
const member = await database.members.create({
|
||||||
|
name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
|
||||||
|
});
|
||||||
|
|
||||||
|
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} [attachmentExpiration] - The attachment expiration date (if any)
|
||||||
|
* @returns {Promise<string>} A successful update.
|
||||||
|
* @throws {Error} When no member row was updated.
|
||||||
|
*/
|
||||||
|
mh.updateMemberField = async function (authorId, memberName, columnName, value, attachmentExpiration = null) {
|
||||||
|
let fluxerPropicWarning;
|
||||||
|
|
||||||
|
// indicates that an attachment was uploaded on Fluxer directly
|
||||||
|
if (columnName === "propic" && attachmentExpiration) {
|
||||||
|
fluxerPropicWarning = mh.setExpirationWarning(value);
|
||||||
|
}
|
||||||
|
return await database.members.update({[columnName]: value}, {
|
||||||
|
where: {
|
||||||
|
name: {[Op.iLike]: memberName},
|
||||||
|
userid: authorId
|
||||||
|
}
|
||||||
|
}).then((res) => {
|
||||||
|
if (res[0] === 0) {
|
||||||
|
throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
|
||||||
|
} else {
|
||||||
|
return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the warning for an expiration date.
|
||||||
|
*
|
||||||
|
* @param {string} expirationString - An expiration date string.
|
||||||
|
* @returns {string} A description of the expiration, interpolating the expiration string.
|
||||||
|
*/
|
||||||
|
mh.setExpirationWarning = function (expirationString) {
|
||||||
|
let expirationDate = new Date(expirationString);
|
||||||
|
if (!isNaN(expirationDate.valueOf())) {
|
||||||
|
expirationDate = expirationDate.toDateString();
|
||||||
|
return `\n**NOTE:** Because this profile picture was uploaded via Fluxer, it will currently expire on *${expirationDate}*. To avoid this, upload the picture to another website like <https://imgbb.com/> and link to it directly`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the details for a member.
|
||||||
|
*
|
||||||
|
* @param {model} member - The member object
|
||||||
|
* @returns {EmbedBuilder} The member's info.
|
||||||
|
*/
|
||||||
|
mh.getMemberInfo = function (member) {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(member.name)
|
||||||
|
.setDescription(`Details for ${member.name}`)
|
||||||
|
.addFields({
|
||||||
|
name: 'Display name: ',
|
||||||
|
value: member.displayname ?? 'unset',
|
||||||
|
inline: true
|
||||||
|
}, {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true},)
|
||||||
|
.setImage(member.propic ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
mh.getAllMembersInfo = async function (authorId, authorName) {
|
||||||
|
const members = await mh.getMembersByAuthor(authorId);
|
||||||
|
if (members == null) throw Error(enums.err.USER_NO_MEMBERS);
|
||||||
|
const fields = [...members.entries()].map(([name, member]) => ({
|
||||||
|
name: member.name, value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, inline: true,
|
||||||
|
}));
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(`${fields > 25 ? "First 25 m" : "M"}embers for ${authorName}`)
|
||||||
|
.addFields(...fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a member based on the author and proxy tag.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message.
|
||||||
|
* @param {string} memberName - The member's name.
|
||||||
|
* @returns {Promise<model>} The member object.
|
||||||
|
*/
|
||||||
|
mh.getMemberByName = async function (authorId, memberName) {
|
||||||
|
return await database.members.findOne({where: {userid: authorId, name: {[Op.iLike]: memberName}}});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all members belonging to the author.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} authorId - The author of the message
|
||||||
|
* @returns {Promise<model[] | null>} The member object array.
|
||||||
|
*/
|
||||||
|
mh.getMembersByAuthor = async function (authorId) {
|
||||||
|
return await database.members.findAll({where: {userid: authorId}});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if proxy exists for a member.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
mh.checkIfProxyExists = async function (authorId, proxy) {
|
||||||
|
const splitProxy = proxy.trim().split("text");
|
||||||
|
if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY);
|
||||||
|
if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER);
|
||||||
|
|
||||||
|
await mh.getMembersByAuthor(authorId).then((memberList) => {
|
||||||
|
const proxyExists = memberList.some(member => member.proxy === proxy);
|
||||||
|
if (proxyExists) {
|
||||||
|
throw new Error(enums.err.PROXY_EXISTS);
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
throw e
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an embed with all member commands
|
||||||
|
*
|
||||||
|
* @returns {EmbedBuilder } An embed of member commands.
|
||||||
|
*/
|
||||||
|
mh.getMemberCommandInfo = function() {
|
||||||
|
const fields = [
|
||||||
|
{name: `**new**`, value: enums.help.NEW, inline: false},
|
||||||
|
{name: `**remove**`, value: enums.help.REMOVE, inline: false},
|
||||||
|
{name: `**name**`, value: enums.help.NAME, inline: false},
|
||||||
|
{name: `**displayname**`, value: enums.help.DISPLAY_NAME, inline: false},
|
||||||
|
{name: `**proxy**`, value: enums.help.PROXY, inline: false},
|
||||||
|
{name: `**propic**`, value: enums.help.PROPIC, inline: false},
|
||||||
|
{name: `**list**`, value: enums.help.LIST, inline: false},
|
||||||
|
];
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle("Member subcommands")
|
||||||
|
.setDescription(enums.help.MEMBER)
|
||||||
|
.addFields(...fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const memberHelper = mh;
|
||||||
84
src/helpers/messageHelper.js
Normal file
84
src/helpers/messageHelper.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {memberHelper} from "./memberHelper.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 {{model, string, bool}} The proxy message object.
|
||||||
|
* @throws {Error} If a proxy message is sent with no message or attachment within it.
|
||||||
|
*/
|
||||||
|
msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){
|
||||||
|
const members = await memberHelper.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}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messageHelper = msgh;
|
||||||
29
src/helpers/utils.js
Normal file
29
src/helpers/utils.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {enums} from '../enums.js'
|
||||||
|
|
||||||
|
const u = {};
|
||||||
|
|
||||||
|
u.debounce = function(func, delay) {
|
||||||
|
let timeout = null;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an uploaded picture is in the right format.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} imageUrl - The url of the image
|
||||||
|
* @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements.
|
||||||
|
*/
|
||||||
|
u.checkImageFormatValidity = async function (imageUrl) {
|
||||||
|
const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp'];
|
||||||
|
await fetch(imageUrl).then(r => r.blob()).then(blobFile => {
|
||||||
|
if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS);
|
||||||
|
}).catch((error) => {
|
||||||
|
throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const utils = u;
|
||||||
103
src/helpers/webhookHelper.js
Normal file
103
src/helpers/webhookHelper.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import {messageHelper} from "./messageHelper.js";
|
||||||
|
import {Webhook, Channel, Message, Client} from '@fluxerjs/core';
|
||||||
|
import {enums} from "../enums.js";
|
||||||
|
|
||||||
|
const wh = {};
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
wh.sendMessageAsMember = async function(client, message) {
|
||||||
|
const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null;
|
||||||
|
const proxyMatch = await messageHelper.parseProxyTags(message.author.id, message.content, attachmentUrl).catch(e =>{throw e});
|
||||||
|
// 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 wh.replaceMessage(client, message, proxyMatch.message, proxyMatch.member).catch(e =>{throw e});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
wh.replaceMessage = async function(client, message, text, member) {
|
||||||
|
// attachment logic is not relevant yet, text length will always be over 0 right now
|
||||||
|
if (text.length > 0 || message.attachments.size > 0) {
|
||||||
|
const channel = client.channels.get(message.channelId);
|
||||||
|
const webhook = await wh.getOrCreateWebhook(client, channel).catch((e) =>{throw e});
|
||||||
|
const username = member.displayname ?? member.name;
|
||||||
|
if (text.length > 0) {
|
||||||
|
await webhook.send({content: text, username: username, avatar_url: member.propic}).catch(async(e) => {
|
||||||
|
const returnedBuffer = messageHelper.returnBufferFromText(text);
|
||||||
|
await webhook.send({content: returnedBuffer.text, username: username, avatar_url: member.propic, files: [{ name: 'text.txt', data: returnedBuffer.file }]
|
||||||
|
})
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (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.
|
||||||
|
*/
|
||||||
|
wh.getOrCreateWebhook = async function(client, channel) {
|
||||||
|
// If channel doesn't allow webhooks
|
||||||
|
if (!channel?.createWebhook) throw new Error(enums.err.NO_WEBHOOKS_ALLOWED);
|
||||||
|
let webhook = await wh.getWebhook(client, channel).catch((e) =>{throw e});
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
wh.getWebhook = async function(client, channel) {
|
||||||
|
const channelWebhooks = await channel?.fetchWebhooks() ?? [];
|
||||||
|
if (channelWebhooks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pf_webhook;
|
||||||
|
channelWebhooks.forEach((webhook) => {
|
||||||
|
if (webhook.name === name) {
|
||||||
|
pf_webhook = webhook;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return pf_webhook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webhookHelper = wh;
|
||||||
322
tests/bot.test.js
Normal file
322
tests/bot.test.js
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
const env = require('dotenv').config({path: './.env.jest'})
|
||||||
|
const {enums} = require("../src/enums.js");
|
||||||
|
|
||||||
|
jest.mock('@fluxerjs/core', () => {
|
||||||
|
return {
|
||||||
|
Events: {
|
||||||
|
MessageCreate: jest.fn(),
|
||||||
|
Ready: jest.fn(),
|
||||||
|
GuildCreate: jest.fn(),
|
||||||
|
},
|
||||||
|
Client: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: jest.fn(),
|
||||||
|
intents: 0,
|
||||||
|
login: jest.fn()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Message: jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("../src/helpers/messageHelper.js", () => {
|
||||||
|
return {
|
||||||
|
messageHelper: {
|
||||||
|
parseCommandArgs: jest.fn(),
|
||||||
|
prefix: "pf;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("../src/helpers/webhookHelper.js", () => {
|
||||||
|
return {
|
||||||
|
webhookHelper: {
|
||||||
|
sendMessageAsMember: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
jest.mock("../src/helpers/utils.js", () => {
|
||||||
|
return {
|
||||||
|
utils: {
|
||||||
|
debounce: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.mock("../src/commands.js", () => {
|
||||||
|
return {
|
||||||
|
commands: {
|
||||||
|
commandsMap: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
aliasesMap: {
|
||||||
|
get: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const {Client, Events} = require('@fluxerjs/core');
|
||||||
|
const {messageHelper} = require("../src/helpers/messageHelper.js");
|
||||||
|
|
||||||
|
const {commands} = require("../src/commands.js");
|
||||||
|
const {webhookHelper} = require("../src/helpers/webhookHelper.js");
|
||||||
|
|
||||||
|
const {utils} = require("../src/helpers/utils.js");
|
||||||
|
let {handleMessageCreate, client} = require("../src/bot.js");
|
||||||
|
|
||||||
|
describe('bot', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('handleMessageCreate', () => {
|
||||||
|
|
||||||
|
test('on message creation, if message is from bot, return', () => {
|
||||||
|
// Arrange
|
||||||
|
const message = {
|
||||||
|
author: {
|
||||||
|
bot: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Act
|
||||||
|
return handleMessageCreate(message).then((res) => {
|
||||||
|
expect(res).toBe(undefined);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test('on message creation, if message is empty, return', () => {
|
||||||
|
// Arrange
|
||||||
|
const message = {
|
||||||
|
content: " ",
|
||||||
|
author: {
|
||||||
|
bot: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Act
|
||||||
|
return handleMessageCreate(message).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toBe(undefined);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test("if message doesn't start with bot prefix, call sendMessageAsMember", () => {
|
||||||
|
// Arrange
|
||||||
|
webhookHelper.sendMessageAsMember.mockResolvedValue();
|
||||||
|
const message = {
|
||||||
|
content: "hello",
|
||||||
|
author: {
|
||||||
|
bot: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Act
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// Assert
|
||||||
|
expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledTimes(1);
|
||||||
|
expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledWith(client, message)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test("if sendMessageAsMember returns error, log error", () => {
|
||||||
|
// Arrange
|
||||||
|
webhookHelper.sendMessageAsMember.mockImplementation(() => {
|
||||||
|
throw Error("error")
|
||||||
|
});
|
||||||
|
const message = {
|
||||||
|
content: "hello",
|
||||||
|
author: {
|
||||||
|
bot: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jest.mock('console', () => {
|
||||||
|
return {error: jest.fn()}
|
||||||
|
})
|
||||||
|
// Act
|
||||||
|
return handleMessageCreate(message).catch(() => {
|
||||||
|
// 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", () => {
|
||||||
|
// Arrange
|
||||||
|
const message = {
|
||||||
|
content: "pf;",
|
||||||
|
author: {
|
||||||
|
bot: false
|
||||||
|
},
|
||||||
|
reply: jest.fn()
|
||||||
|
}
|
||||||
|
// Act
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// 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", () => {
|
||||||
|
// 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
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// 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', () => {
|
||||||
|
// 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
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// 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', () => {
|
||||||
|
// 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
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// Assert
|
||||||
|
expect(commands.commandsMap.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(commands.commandsMap.get).toHaveBeenNthCalledWith(1, 'm');
|
||||||
|
expect(commands.aliasesMap.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(commands.aliasesMap.get).toHaveBeenCalledWith('m');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test("if command exists, call command.execute", () => {
|
||||||
|
// 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
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// 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", () => {
|
||||||
|
// Arrange
|
||||||
|
const command = {
|
||||||
|
execute: jest.fn()
|
||||||
|
}
|
||||||
|
commands.get = jest.fn().mockReturnValue(command);
|
||||||
|
command.execute.mockImplementation(() => {
|
||||||
|
throw Error("error")
|
||||||
|
});
|
||||||
|
const message = {
|
||||||
|
content: "pf;member test",
|
||||||
|
author: {
|
||||||
|
bot: false
|
||||||
|
},
|
||||||
|
reply: jest.fn()
|
||||||
|
}
|
||||||
|
jest.mock('console', () => {
|
||||||
|
return {error: jest.fn()}
|
||||||
|
})
|
||||||
|
// Act
|
||||||
|
return handleMessageCreate(message).catch(() => {
|
||||||
|
// Assert
|
||||||
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
|
expect(console.error).toHaveBeenCalledWith(new Error('error'))
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test("if command does not exist, return correct enum", () => {
|
||||||
|
// 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
|
||||||
|
return handleMessageCreate(message).then(() => {
|
||||||
|
// Assert
|
||||||
|
expect(message.reply).toHaveBeenCalledWith(enums.err.COMMAND_NOT_RECOGNIZED);
|
||||||
|
expect(message.reply).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls client.login with correct argument', () => {
|
||||||
|
// Act
|
||||||
|
client.login = jest.fn().mockResolvedValue();
|
||||||
|
// Assert
|
||||||
|
expect(client.login).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client.login).toHaveBeenCalledWith(process.env.FLUXER_BOT_TOKEN)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
183
tests/commands.test.js
Normal file
183
tests/commands.test.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
jest.mock('console', () => {
|
||||||
|
return {error: jest.fn()}
|
||||||
|
})
|
||||||
|
|
||||||
|
import {messageHelper, prefix} 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.png';
|
||||||
|
const attachmentExpiration = new Date('2026-01-01').toDateString();
|
||||||
|
const message = {
|
||||||
|
author: {
|
||||||
|
username: username,
|
||||||
|
id: authorId,
|
||||||
|
discriminator: discriminator,
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
size: 1,
|
||||||
|
first: jest.fn().mockImplementation(() => ({
|
||||||
|
expires_at: attachmentExpiration,
|
||||||
|
url: attachmentUrl
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
reply: jest.fn().mockResolvedValue(),
|
||||||
|
}
|
||||||
|
const args = ['new']
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('memberCommand', () => {
|
||||||
|
|
||||||
|
|
||||||
|
test('calls parseMemberCommand with the correct arguments', () => {
|
||||||
|
// Arrange
|
||||||
|
memberHelper.parseMemberCommand = jest.fn().mockResolvedValue("parsed command");
|
||||||
|
// Act
|
||||||
|
return commands.memberCommand(message, args).then(() => {
|
||||||
|
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', () => {
|
||||||
|
// Arrange
|
||||||
|
memberHelper.parseMemberCommand = jest.fn().mockImplementation(() => {throw new Error('error')});
|
||||||
|
// Act
|
||||||
|
return commands.memberCommand(message, args).catch(() => {
|
||||||
|
expect(message.reply).toHaveBeenCalledTimes(1);
|
||||||
|
expect(message.reply).toHaveBeenCalledWith('error');
|
||||||
|
expect(console.error).toHaveBeenCalledWith(new Error('error'));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if parseMemberCommand returns embed, reply with embed', () => {
|
||||||
|
// Arrange
|
||||||
|
const embed = new EmbedBuilder();
|
||||||
|
memberHelper.parseMemberCommand = jest.fn().mockResolvedValue();
|
||||||
|
// Act
|
||||||
|
return commands.memberCommand(message, args).catch(() => {
|
||||||
|
// Assert
|
||||||
|
expect(message.reply).toHaveBeenCalledTimes(1);
|
||||||
|
expect(message.reply).toHaveBeenCalledWith({embeds: [embed]})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if parseMemberCommand returns object, reply with embed and content', () => {
|
||||||
|
// Arrange
|
||||||
|
const reply = {
|
||||||
|
errors: ['error', 'error2'],
|
||||||
|
success: 'success',
|
||||||
|
embed: {}
|
||||||
|
}
|
||||||
|
memberHelper.parseMemberCommand = jest.fn().mockResolvedValue(reply);
|
||||||
|
// Act
|
||||||
|
return commands.memberCommand(message, args).catch(() => {
|
||||||
|
// Assert
|
||||||
|
expect(message.reply).toHaveBeenCalledTimes(1);
|
||||||
|
expect(message.reply).toHaveBeenCalledWith({content: `success\n\n${enums.err.ERRORS_OCCURRED}\n\nerror\nerror2}`, embeds: [reply.embed]})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('importCommand', () => {
|
||||||
|
test('if message includes --help and no attachmentURL, return help message', () => {
|
||||||
|
const args = ["--help"];
|
||||||
|
message.content = "pf;import --help";
|
||||||
|
message.attachments.size = 0;
|
||||||
|
return commands.importCommand(message, args).then(() => {
|
||||||
|
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', () => {
|
||||||
|
const args = [""];
|
||||||
|
message.content = 'pf;import'
|
||||||
|
message.attachments.size = 0;
|
||||||
|
return commands.importCommand(message, args).then(() => {
|
||||||
|
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', () => {
|
||||||
|
const args = [""];
|
||||||
|
message.content = 'pf;import'
|
||||||
|
importHelper.pluralKitImport = jest.fn().mockResolvedValue('success');
|
||||||
|
return commands.importCommand(message, args).then(() => {
|
||||||
|
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, send errors.', () => {
|
||||||
|
const args = [""];
|
||||||
|
message.content = 'pf;import'
|
||||||
|
importHelper.pluralKitImport = jest.fn().mockImplementation(() => {throw new AggregateError(['error1', 'error2'], 'errors')});
|
||||||
|
return commands.importCommand(message, args).catch(() => {
|
||||||
|
expect(message.reply).toHaveBeenCalledTimes(1);
|
||||||
|
expect(message.reply).toHaveBeenCalledWith(`errors. \n\n${enums.err.ERRORS_OCCURRED}\n\nerror1\nerror2`);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if message.reply throws error, call returnBufferFromText and message.reply again.', () => {
|
||||||
|
// Arrange
|
||||||
|
const args = [""];
|
||||||
|
message.content = 'pf;import'
|
||||||
|
message.reply = jest.fn().mockImplementationOnce(() => {throw e})
|
||||||
|
messageHelper.returnBufferFromText = jest.fn().mockResolvedValue({file: 'test.txt', text: 'normal content'});
|
||||||
|
return commands.importCommand(message, args).catch(() => {
|
||||||
|
expect(message.reply).toHaveBeenCalledTimes(2);
|
||||||
|
expect(message.reply).toHaveBeenNthCalledWith(1, {content: 'normal content', files: [{name: 'test.txt', data: 'test.txt' }],});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
100
tests/helpers/importHelper.test.js
Normal file
100
tests/helpers/importHelper.test.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const {enums} = require('../../src/enums.js');
|
||||||
|
const fetchMock = require('jest-fetch-mock');
|
||||||
|
|
||||||
|
jest.mock('../../src/helpers/memberHelper.js', () => {
|
||||||
|
return {
|
||||||
|
memberHelper: {
|
||||||
|
addFullMember: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchMock.enableMocks();
|
||||||
|
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(() => {
|
||||||
|
global.fetch = jest.fn();
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockData)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('pluralKitImport', () => {
|
||||||
|
|
||||||
|
test('if no attachment URL, throws error', () => {
|
||||||
|
return importHelper.pluralKitImport(authorId).catch((e) => {
|
||||||
|
expect(e).toEqual(new Error(enums.err.NOT_JSON_FILE));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if attachment URL, calls fetch and addFullMember and returns value', () => {
|
||||||
|
memberHelper.addFullMember.mockResolvedValue(mockAddReturn);
|
||||||
|
return importHelper.pluralKitImport(authorId, attachmentUrl).then((res) => {
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fetch).toHaveBeenCalledWith(attachmentUrl);
|
||||||
|
expect(memberHelper.addFullMember).toHaveBeenCalledWith(authorId, mockImportedMember.name, mockImportedMember.display_name, 'SP{text}', mockImportedMember.avatar_url);
|
||||||
|
expect(res).toEqual(`Successfully added members: ${mockAddReturnMember.name}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if addFullMember returns nothing, return correct enum', () => {
|
||||||
|
memberHelper.addFullMember.mockResolvedValue();
|
||||||
|
return importHelper.pluralKitImport(authorId, attachmentUrl).catch((res) => {
|
||||||
|
expect(res).toEqual(new AggregateError([], enums.err.NO_MEMBERS_IMPORTED));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if addFullMember returns nothing and throws error, catch and return error', () => {
|
||||||
|
memberHelper.addFullMember.mockResolvedValue(new Error('error'));
|
||||||
|
return importHelper.pluralKitImport(authorId, attachmentUrl).catch((res) => {
|
||||||
|
expect(res).toEqual(new AggregateError([new Error('error')], enums.err.NO_MEMBERS_IMPORTED))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if addFullMember returns member but also contains error, return member and error', () => {
|
||||||
|
// Arrange
|
||||||
|
const memberObj = {errors: ['error'], member: mockAddReturnMember};
|
||||||
|
memberHelper.addFullMember.mockResolvedValue(memberObj);
|
||||||
|
// Act
|
||||||
|
return importHelper.pluralKitImport(authorId, attachmentUrl).catch((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(new AggregateError(['error'], `Successfully added members: ${mockAddReturnMember.name}`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
778
tests/helpers/memberHelper.test.js
Normal file
778
tests/helpers/memberHelper.test.js
Normal file
@@ -0,0 +1,778 @@
|
|||||||
|
const {enums} = require('../../src/enums.js');
|
||||||
|
const {utils} = require("../../src/helpers/utils.js");
|
||||||
|
|
||||||
|
jest.mock('@fluxerjs/core', () => jest.fn());
|
||||||
|
jest.mock('../../src/database.js', () => {
|
||||||
|
return {
|
||||||
|
database: {
|
||||||
|
members: {
|
||||||
|
create: jest.fn().mockResolvedValue(),
|
||||||
|
update: jest.fn().mockResolvedValue(),
|
||||||
|
destroy: jest.fn().mockResolvedValue(),
|
||||||
|
findOne: jest.fn().mockResolvedValue(),
|
||||||
|
findAll: jest.fn().mockResolvedValue(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("../../src/helpers/utils.js", () => {
|
||||||
|
return {
|
||||||
|
utils:
|
||||||
|
{
|
||||||
|
checkImageFormatValidity: jest.fn().mockResolvedValue(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {Op} = require('sequelize');
|
||||||
|
|
||||||
|
const {memberHelper} = require("../../src/helpers/memberHelper.js");
|
||||||
|
const {database} = require("../../src/database");
|
||||||
|
|
||||||
|
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: attachmentUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => {
|
||||||
|
// 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', (args, attachmentUrl, attachmentExpiration, command, isHelp, memberName) => {
|
||||||
|
console.log(args, command, isHelp)
|
||||||
|
// Act
|
||||||
|
return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration).then((result) => {
|
||||||
|
// 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 return command not recognized enum', () => {
|
||||||
|
// Arrange
|
||||||
|
return memberHelper.memberArgumentHandler(authorId, authorFull, false, null, null, []).catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new Error(enums.err.COMMAND_NOT_RECOGNIZED));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['new'],
|
||||||
|
['remove'],
|
||||||
|
['name'],
|
||||||
|
['displayname'],
|
||||||
|
['proxy'],
|
||||||
|
['propic'],
|
||||||
|
])('when %s is present but other values are null, should return no member enum', (command) => {
|
||||||
|
// Arrange
|
||||||
|
return memberHelper.memberArgumentHandler(authorId, authorFull, false, command, null, []).catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new Error(enums.err.NO_MEMBER));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['new'],
|
||||||
|
['remove'],
|
||||||
|
['name'],
|
||||||
|
['list'],
|
||||||
|
['displayname'],
|
||||||
|
['proxy'],
|
||||||
|
['propic'],
|
||||||
|
])('%s calls sendHelpEnum', (command) => {
|
||||||
|
// Arrange
|
||||||
|
return memberHelper.memberArgumentHandler(authorId, authorFull, true, command, mockMember.name, []).then((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual("help enum");
|
||||||
|
expect(memberHelper.sendHelpEnum).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.sendHelpEnum).toHaveBeenCalledWith(command);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test('list should call getAllMembersInfo', () => {
|
||||||
|
// Arrange
|
||||||
|
return memberHelper.memberArgumentHandler(authorId, authorFull, false, 'list', mockMember.name, []).then((result) => {
|
||||||
|
// 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', (args, attachmentUrl, attachmentExpiration, command) => {
|
||||||
|
// Arrange
|
||||||
|
let values = args.slice(2);
|
||||||
|
|
||||||
|
return memberHelper.memberArgumentHandler(authorId, authorFull, false, command, mockMember.name, args, attachmentUrl, attachmentExpiration).then((result) => {
|
||||||
|
// 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', (command) => {
|
||||||
|
return memberHelper.memberArgumentHandler(authorId, authorFull, false, command, mockMember.name, []).then((result) => {
|
||||||
|
// 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', (command, expected) => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember);
|
||||||
|
// Act
|
||||||
|
return memberHelper.sendCurrentValue(authorId, mockMember.name, command).then((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId,mockMember.name);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns error if no member found', () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(null);
|
||||||
|
// Act
|
||||||
|
return memberHelper.sendCurrentValue(authorId, mockMember.name, 'name').catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new Error(enums.err.NO_MEMBER));
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId,mockMember.name);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls getMemberInfo with member if no command present', () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember);
|
||||||
|
jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue('member info');
|
||||||
|
// Act
|
||||||
|
return memberHelper.sendCurrentValue(authorId, mockMember.name, null).then((result) => {
|
||||||
|
// 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 member found', (command, expected) => {
|
||||||
|
// Arrange
|
||||||
|
const empty = {name: mockMember.name, displayname: null, proxy: null, propic: null}
|
||||||
|
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(empty);
|
||||||
|
// Act
|
||||||
|
return memberHelper.sendCurrentValue(authorId, mockMember.name, command).then((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.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
|
||||||
|
return memberHelper.addNewMember(authorId, mockMember.name, args, attachmentUrl, attachmentExpiration).then(() => {
|
||||||
|
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
|
||||||
|
return memberHelper.addNewMember(authorId, mockMember.name, args, attachmentUrl, attachmentExpiration).then((result) => {
|
||||||
|
// 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
|
||||||
|
return memberHelper.addNewMember(authorId, mockMember.name, args).catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new Error('getMemberInfo error'));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws expected error when addFullMember throws error', async () => {
|
||||||
|
// Arrange
|
||||||
|
const args = [];
|
||||||
|
const expected = 'add full member error';
|
||||||
|
jest.spyOn(memberHelper, 'addFullMember').mockImplementation(() => {
|
||||||
|
throw new Error(expected)
|
||||||
|
});
|
||||||
|
|
||||||
|
//Act
|
||||||
|
return memberHelper.addNewMember(authorId, mockMember.name, args).catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new Error(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
|
||||||
|
return memberHelper.updateName(authorId, mockMember.name, " somePerson ").then((result) => {
|
||||||
|
// 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
|
||||||
|
return memberHelper.updateName(authorId, mockMember.name, " ").catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE));
|
||||||
|
expect(memberHelper.updateMemberField).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateDisplayName', () => {
|
||||||
|
|
||||||
|
test('throws error when displayname is blank', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue();
|
||||||
|
// Act
|
||||||
|
return memberHelper.updateDisplayName(authorId, mockMember.name, mockMember.displayname).catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new Error(`Display name ${enums.err.NO_VALUE}`));
|
||||||
|
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
|
||||||
|
return memberHelper.updateDisplayName(authorId, mockMember.name, tooLongDisplayName).catch((result) => {
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(new RangeError(enums.err.DISPLAY_NAME_TOO_LONG));
|
||||||
|
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
|
||||||
|
return memberHelper.updateDisplayName(authorId, mockMember.name, " Some Person ").then((result) => {
|
||||||
|
// 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
|
||||||
|
return memberHelper.updateProxy(authorId, mockMember.name, "--text").then((result) => {
|
||||||
|
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, null, attachmentUrl],
|
||||||
|
[mockMember.propic, null, null, mockMember.propic],
|
||||||
|
[mockMember.propic, attachmentUrl, null, attachmentUrl],
|
||||||
|
[null, attachmentUrl, attachmentExpiration, attachmentUrl]
|
||||||
|
])('calls checkImageFormatValidity and updateMemberField and returns string', async(imgUrl, attachmentUrl, attachmentExpiration, expected) => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated");
|
||||||
|
// Act
|
||||||
|
return memberHelper.updatePropic(authorId, mockMember.name, imgUrl, attachmentUrl, attachmentExpiration).then((result) => {
|
||||||
|
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, attachmentExpiration);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addFullMember', () => {
|
||||||
|
const { database} = require('../../src/database.js');
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue();
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls getMemberByName', async () => {
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name).then(() => {
|
||||||
|
// Assert
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
|
||||||
|
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if getMemberByName returns member, throw error', async () => {
|
||||||
|
memberHelper.getMemberByName.mockResolvedValue({name: mockMember.name});
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name).catch((e) => {
|
||||||
|
// Assert
|
||||||
|
expect(e).toEqual(new Error(`Can't add ${mockMember.name}. ${enums.err.MEMBER_EXISTS}`))
|
||||||
|
expect(database.members.create).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('if name is not filled out, throw error', async () => {
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, " ").catch((e) => {
|
||||||
|
// Assert
|
||||||
|
expect(e).toEqual(new Error(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`))
|
||||||
|
expect(database.members.create).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if displayname is over 32 characters, call database.member.create with null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
memberHelper.getMemberByName.mockResolvedValue();
|
||||||
|
const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
database.members.create = 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
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name, tooLongDisplayName, null, null).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(database.members.create).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if proxy, call checkIfProxyExists', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue();
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: mockMember.proxy,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {member: expectedMemberArgs, errors: []}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy);
|
||||||
|
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1);
|
||||||
|
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(database.members.create).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if checkProxyExists throws error, call database.member.create with null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockImplementation(() => {
|
||||||
|
throw new Error('error')
|
||||||
|
});
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {
|
||||||
|
member: expectedMemberArgs,
|
||||||
|
errors: [`Tried to set proxy to \"${mockMember.proxy}\". error. ${enums.err.SET_TO_NULL}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy, null).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(database.members.create).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if propic, call checkImageFormatValidity', async () => {
|
||||||
|
// Arrange
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: mockMember.propic
|
||||||
|
}
|
||||||
|
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {member: expectedMemberArgs, errors: []}
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(mockMember.propic);
|
||||||
|
expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1);
|
||||||
|
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(database.members.create).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if checkImageFormatValidity throws error, call database.member.create with null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
utils.checkImageFormatValidity = jest.fn().mockImplementation(() => {throw new Error("error")})
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: null,
|
||||||
|
proxy: null,
|
||||||
|
propic: null
|
||||||
|
}
|
||||||
|
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
const expectedReturn = {
|
||||||
|
member: expectedMemberArgs,
|
||||||
|
errors: [`Tried to set profile picture to \"${mockMember.propic}\". error. ${enums.err.SET_TO_NULL}`]
|
||||||
|
}
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(database.members.create).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if all values are valid, call database.members.create', async () => {
|
||||||
|
// Arrange
|
||||||
|
jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue();
|
||||||
|
const expectedMemberArgs = {
|
||||||
|
name: mockMember.name,
|
||||||
|
userid: authorId,
|
||||||
|
displayname: mockMember.displayname,
|
||||||
|
proxy: mockMember.proxy,
|
||||||
|
propic: mockMember.propic
|
||||||
|
}
|
||||||
|
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs);
|
||||||
|
utils.checkImageFormatValidity = jest.fn().mockResolvedValue();
|
||||||
|
const expectedReturn = {member: expectedMemberArgs, errors: []}
|
||||||
|
// Act
|
||||||
|
return await memberHelper.addFullMember(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(expectedReturn);
|
||||||
|
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs);
|
||||||
|
expect(database.members.create).toHaveBeenCalledTimes(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateMemberField', () => {
|
||||||
|
const {database} = require('../../src/database.js');
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(memberHelper, "setExpirationWarning").mockReturnValue(' warning');
|
||||||
|
database.members = {
|
||||||
|
update: jest.fn().mockResolvedValue([1])
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls setExpirationWarning if attachmentExpiration', async () => {
|
||||||
|
return memberHelper.updateMemberField(authorId, mockMember.name, "propic", mockMember.propic, attachmentExpiration).then((res) => {
|
||||||
|
expect(memberHelper.setExpirationWarning).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['name', mockMember.name, null, `Updated name for ${mockMember.name} to ${mockMember.name}`],
|
||||||
|
['displayname', mockMember.displayname, null, `Updated name for ${mockMember.name} to ${mockMember.displayname}`],
|
||||||
|
['proxy', mockMember.proxy, null, `Updated name for ${mockMember.name} to ${mockMember.proxy}`],
|
||||||
|
['propic', mockMember.propic, null, `Updated name for ${mockMember.name} to ${mockMember.propic}`],
|
||||||
|
['propic', mockMember.propic, attachmentExpiration, `Updated name for ${mockMember.name} to ${mockMember.propic} warning}`]
|
||||||
|
])('calls database.members.update with correct column and value and return string', async (columnName, value, attachmentExpiration) => {
|
||||||
|
// Arrange
|
||||||
|
return memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration).then((res) => {
|
||||||
|
// Act
|
||||||
|
expect(database.members.update).toHaveBeenCalledTimes(1);
|
||||||
|
expect(database.members.update).toHaveBeenCalledWith({[columnName]: value}, {
|
||||||
|
where: {
|
||||||
|
name: {[Op.iLike]: mockMember.name},
|
||||||
|
userid: authorId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if database.members.update returns 0 rows changed, throw error', () => {
|
||||||
|
// Arrange
|
||||||
|
database.members = {
|
||||||
|
update: jest.fn().mockResolvedValue([0])
|
||||||
|
};
|
||||||
|
// Act
|
||||||
|
return memberHelper.updateMemberField(authorId, mockMember.name, "displayname", mockMember.displayname).catch((res) => {
|
||||||
|
expect(res).toEqual(new Error(`Can't update ${mockMember.name}. ${enums.err.NO_MEMBER}.`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkIfProxyExists', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(memberHelper, "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) => {
|
||||||
|
return memberHelper.checkIfProxyExists(authorId, proxy).then((res) => {
|
||||||
|
expect(res).toEqual(false)
|
||||||
|
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.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],
|
||||||
|
['--text', enums.err.PROXY_EXISTS, true]
|
||||||
|
])('%s returns correct error and calls getMembersByAuthor if appropriate', async (proxy, error, shouldCall) => {
|
||||||
|
return memberHelper.checkIfProxyExists(authorId, proxy).catch((res) => {
|
||||||
|
expect(res).toEqual(new Error(error))
|
||||||
|
if (shouldCall) {
|
||||||
|
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(memberHelper.getMembersByAuthor).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
120
tests/helpers/messageHelper.test.js
Normal file
120
tests/helpers/messageHelper.test.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const env = require('dotenv');
|
||||||
|
env.config();
|
||||||
|
|
||||||
|
|
||||||
|
jest.mock('../../src/helpers/memberHelper.js', () => {
|
||||||
|
return {memberHelper: {
|
||||||
|
getMembersByAuthor: jest.fn()
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
|
||||||
|
const {memberHelper} = require("../../src/helpers/memberHelper.js");
|
||||||
|
const {messageHelper} = require("../../src/helpers/messageHelper.js");
|
||||||
|
|
||||||
|
describe('messageHelper', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseCommandArgs', () => {
|
||||||
|
test.each([
|
||||||
|
['pk;member', ['']],
|
||||||
|
['pk;member add somePerson "Some Person"', ['add', 'somePerson', 'Some Person']],
|
||||||
|
['pk;member add \"Some Person\"', ['add', 'Some Person']],
|
||||||
|
['pk;member add somePerson \'Some Person\'', ['add', 'somePerson', 'Some Person']],
|
||||||
|
['pk;member add somePerson \"\'Some\' Person\"', ['add', 'somePerson', 'Some Person']],
|
||||||
|
])('%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(() => {
|
||||||
|
memberHelper.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
|
||||||
|
return messageHelper.parseProxyTags(specificAuthorId, content, attachmentUrl).then((res) => {
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
})
|
||||||
19
tests/helpers/utils.test.js
Normal file
19
tests/helpers/utils.test.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const {enums} = require("../../src/enums");
|
||||||
|
|
||||||
|
const fetchMock = require('jest-fetch-mock');
|
||||||
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
|
const {utils} = require("../../src/helpers/utils.js");
|
||||||
|
|
||||||
|
describe('utils', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
269
tests/helpers/webhookHelper.test.js
Normal file
269
tests/helpers/webhookHelper.test.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
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', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`sendMessageAsMember`, () => {
|
||||||
|
const client = {};
|
||||||
|
const content = "hi"
|
||||||
|
const attachments = {
|
||||||
|
size: 0,
|
||||||
|
first: () => {}
|
||||||
|
}
|
||||||
|
const message = {
|
||||||
|
client,
|
||||||
|
content: content,
|
||||||
|
attachments: attachments,
|
||||||
|
author: {
|
||||||
|
id: '123'
|
||||||
|
},
|
||||||
|
guild: {
|
||||||
|
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
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
|
||||||
|
expect(res).toBeUndefined();
|
||||||
|
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
|
||||||
|
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null);
|
||||||
|
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls parseProxyTags and returns if proxyMatch is undefined', async() => {
|
||||||
|
// Arrange
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(undefined);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toBeUndefined();
|
||||||
|
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
|
||||||
|
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, null);
|
||||||
|
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls parseProxyTags with attachmentUrl', async() => {
|
||||||
|
// Arrange
|
||||||
|
message.attachments = {
|
||||||
|
size: 1,
|
||||||
|
first: () => {
|
||||||
|
return {url: 'oya.png'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// message.attachments.set('attachment', {url: 'oya.png'})
|
||||||
|
// message.attachments.set('first', () => {return {url: 'oya.png'}})
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(undefined);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toBeUndefined();
|
||||||
|
expect(messageHelper.parseProxyTags).toHaveBeenCalledTimes(1);
|
||||||
|
expect(messageHelper.parseProxyTags).toHaveBeenCalledWith(message.author.id, content, 'oya.png');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if message matches member proxy but is not sent from a guild, throw an error', async() => {
|
||||||
|
// Arrange
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).catch((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(res).toEqual(new Error(enums.err.NOT_IN_SERVER));
|
||||||
|
expect(webhookHelper.replaceMessage).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
|
||||||
|
// 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
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(message.reply).not.toHaveBeenCalled();
|
||||||
|
expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if replace message throws error, throw same error', async() => {
|
||||||
|
// Arrange
|
||||||
|
message.guildId = '123';
|
||||||
|
messageHelper.parseProxyTags.mockResolvedValue(proxyMessage);
|
||||||
|
jest.spyOn(webhookHelper, 'replaceMessage').mockImplementation(() => {throw new Error("error")});
|
||||||
|
// Act
|
||||||
|
return webhookHelper.sendMessageAsMember(client, message).catch((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(message.reply).not.toHaveBeenCalled();
|
||||||
|
expect(webhookHelper.replaceMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(webhookHelper.replaceMessage).toHaveBeenCalledWith(client, message, proxyMessage.message, proxyMessage.member);
|
||||||
|
expect(res).toEqual(new Error('error'));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`replaceMessage`, () => {
|
||||||
|
const channelId = '123';
|
||||||
|
const authorId = '456';
|
||||||
|
const guildId = '789';
|
||||||
|
const text = "hello";
|
||||||
|
const client = {
|
||||||
|
channels: {
|
||||||
|
get: jest.fn().mockReturnValue(channelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const member = {proxy: "--text", name: 'somePerson', displayname: "Some Person", propic: 'oya.png'};
|
||||||
|
const attachments= {
|
||||||
|
size: 1,
|
||||||
|
first: () => {return channelId;}
|
||||||
|
};
|
||||||
|
const message = {
|
||||||
|
client,
|
||||||
|
channelId: channelId,
|
||||||
|
content: text,
|
||||||
|
attachments: attachments,
|
||||||
|
author: {
|
||||||
|
id: authorId
|
||||||
|
},
|
||||||
|
guild: {
|
||||||
|
guildId: guildId
|
||||||
|
},
|
||||||
|
reply: jest.fn(),
|
||||||
|
delete: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = {
|
||||||
|
send: async() => jest.fn().mockResolvedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
test('does not call anything if text is 0 or message has no attachments', async() => {
|
||||||
|
// Arrange
|
||||||
|
const emptyText = ''
|
||||||
|
const noAttachments = {
|
||||||
|
size: 0,
|
||||||
|
first: () => {}
|
||||||
|
}
|
||||||
|
message.attachments = noAttachments;
|
||||||
|
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.replaceMessage(client, message, emptyText, member).then(() => {
|
||||||
|
expect(webhookHelper.getOrCreateWebhook).not.toHaveBeenCalled();
|
||||||
|
expect(message.delete).not.toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls getOrCreateWebhook and message.delete with correct arguments if text >= 0', async() => {
|
||||||
|
// Arrange
|
||||||
|
message.attachments = {
|
||||||
|
size: 0,
|
||||||
|
first: () => {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.replaceMessage(client, message, text, member).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1);
|
||||||
|
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId);
|
||||||
|
expect(message.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(message.delete).toHaveBeenCalledWith();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: flaky for some reason
|
||||||
|
test('calls getOrCreateWebhook and message.delete with correct arguments if attachments exist', async() => {
|
||||||
|
// Arrange
|
||||||
|
const emptyText = ''
|
||||||
|
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.replaceMessage(client, message, emptyText, member).then((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledTimes(1);
|
||||||
|
expect(webhookHelper.getOrCreateWebhook).toHaveBeenCalledWith(client, channelId);
|
||||||
|
expect(message.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(message.delete).toHaveBeenCalledWith();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls returnBufferFromText and console error if webhook.send returns error', async() => {
|
||||||
|
// Arrange
|
||||||
|
const file = Buffer.from(text, 'utf-8');
|
||||||
|
const returnedBuffer = {text: text, file: file};
|
||||||
|
const expected2ndSend = {content: returnedBuffer.text, username: member.displayname, avatar_url: member.propic, files: [{name: 'text.txt', data: returnedBuffer.file}]};
|
||||||
|
jest.mock('console', () => ({error: jest.fn()}));
|
||||||
|
jest.spyOn(webhookHelper, 'getOrCreateWebhook').mockResolvedValue(webhook);
|
||||||
|
webhook.send = jest.fn().mockImplementationOnce(async() => {throw new Error('error')});
|
||||||
|
messageHelper.returnBufferFromText = jest.fn().mockResolvedValue(returnedBuffer);
|
||||||
|
// Act
|
||||||
|
return webhookHelper.replaceMessage(client, message, text, member).catch((res) => {
|
||||||
|
// Assert
|
||||||
|
expect(messageHelper.returnBufferFromText).toHaveBeenCalledTimes(1);
|
||||||
|
expect(messageHelper.returnBufferFromText).toHaveBeenCalledWith(text);
|
||||||
|
expect(webhook.send).toHaveBeenCalledTimes(2);
|
||||||
|
expect(webhook.send).toHaveBeenNthCalledWith(2, expected2ndSend);
|
||||||
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
|
expect(console.error).toHaveBeenCalledWith(new Error('error'));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`getOrCreateWebhook`, () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`getWebhook`, () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// restore the spy created with spyOn
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user