Compare commits

..

55 commits

Author SHA1 Message Date
ff9437ba81 Update .env.example variables
Some checks failed
Test / build (push) Failing after 27s
2024-12-23 16:20:54 +00:00
9a2835a0eb Add chance effect function 2024-12-23 16:18:40 +00:00
dd1f259170 Add cancel button event
Some checks failed
Test / build (push) Failing after 28s
2024-12-21 15:55:04 +00:00
b37c087393 Add confirmation button event 2024-12-21 15:46:18 +00:00
d874cb7a12 WIP: Start creating confirmation embed
Some checks failed
Test / build (push) Failing after 18s
2024-12-16 19:32:28 +00:00
57c3d603a9 Add check for cooldown 2024-12-16 19:17:23 +00:00
f8b013a091 Create effect use command
Some checks failed
Test / build (push) Failing after 29s
2024-12-13 18:26:02 +00:00
ed52f3e3dc Use node 20
All checks were successful
Deploy To Stage / build (push) Successful in 46s
Deploy To Stage / deploy (push) Successful in 15s
2024-12-10 11:06:01 +00:00
1cace42983 Use node 20 2024-12-10 11:00:11 +00:00
5db7cd9f11 Use node v22 2024-12-10 10:57:50 +00:00
2b96c7c0d3 Merge branch 'main' into develop 2024-12-10 10:44:00 +00:00
3d143e7c73 Create list effects command (#412)
All checks were successful
Deploy To Stage / build (push) Successful in 14s
Deploy To Stage / deploy (push) Successful in 16s
# Description

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.

#379

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration.

# Checklist

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that provde my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

Reviewed-on: #412
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-12-07 22:32:19 +00:00
d7a5472759 Create effects concept (#402)
All checks were successful
Deploy To Stage / build (push) Successful in 19s
Deploy To Stage / deploy (push) Successful in 14s
# Description

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.

#378

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration.

# Checklist

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that provde my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

Reviewed-on: #402
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-11-09 21:31:11 +00:00
6c17a67d7a Fix linter
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 14s
2024-10-28 12:06:27 +00:00
e67efd4197 Upgrade packages
Some checks failed
Deploy To Stage / build (push) Failing after 9s
Deploy To Stage / deploy (push) Has been skipped
2024-10-28 12:03:12 +00:00
9e963d90cb Update dependency typescript to v5.6.3 (#399)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 14s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [typescript](https://www.typescriptlang.org/) ([source](https://github.com/microsoft/TypeScript)) | devDependencies | patch | [`5.6.2` -> `5.6.3`](https://renovatebot.com/diffs/npm/typescript/5.6.2/5.6.3) |

---

### Release Notes

<details>
<summary>microsoft/TypeScript (typescript)</summary>

### [`v5.6.3`](https://github.com/microsoft/TypeScript/releases/tag/v5.6.3): TypeScript 5.6.3

[Compare Source](https://github.com/microsoft/TypeScript/compare/v5.6.2...v5.6.3)

For release notes, check out the [release announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/).

For the complete list of fixed issues, check out the

-   [fixed issues query for Typescript 5.6.0 (Beta)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.0%22+is%3Aclosed+).
-   [fixed issues query for Typescript 5.6.1 (RC)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.1%22+is%3Aclosed+).
-   [fixed issues query for Typescript 5.6.2 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.2%22+is%3Aclosed+).
-   [fixed issues query for Typescript 5.6.3 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.3%22+is%3Aclosed+).

Downloads are available on:

-   [npm](https://www.npmjs.com/package/typescript)
-   [NuGet package](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #399
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-24 17:49:55 +01:00
27c0e68f7a Update dependency @types/node to v20.16.13 (#398)
All checks were successful
Deploy To Stage / build (push) Successful in 17s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.16.11` -> `20.16.13`](https://renovatebot.com/diffs/npm/@types%2fnode/20.16.11/20.16.13) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #398
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-24 17:48:09 +01:00
4c322f01de Update dependency express to v4.21.1 (#392)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 14s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [express](http://expressjs.com/) ([source](https://github.com/expressjs/express)) | dependencies | patch | [`4.21.0` -> `4.21.1`](https://renovatebot.com/diffs/npm/express/4.21.0/4.21.1) |

---

### Release Notes

<details>
<summary>expressjs/express (express)</summary>

### [`v4.21.1`](https://github.com/expressjs/express/releases/tag/4.21.1)

[Compare Source](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

#### What's Changed

-   Backport a fix for CVE-2024-47764 to the 4.x branch by [@&#8203;joshbuker](https://github.com/joshbuker) in https://github.com/expressjs/express/pull/6029
-   Release: 4.21.1 by [@&#8203;UlisesGascon](https://github.com/UlisesGascon) in https://github.com/expressjs/express/pull/6031

**Full Changelog**: https://github.com/expressjs/express/compare/4.21.0...4.21.1

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #392
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-15 18:55:49 +01:00
3304779297 Update dependency @types/node to v20.16.11 (#391)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.16.10` -> `20.16.11`](https://renovatebot.com/diffs/npm/@types%2fnode/20.16.10/20.16.11) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #391
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-15 18:54:29 +01:00
73caf9315d Fix build
All checks were successful
Deploy To Stage / build (push) Successful in 12s
Deploy To Stage / deploy (push) Successful in 16s
2024-10-12 20:56:13 +01:00
76af70da06 Merge branch 'main' into develop
Some checks failed
Deploy To Stage / build (push) Failing after 7s
Deploy To Stage / deploy (push) Has been skipped
2024-10-12 20:53:47 +01:00
8352b377bb Create ability to drop multiple cards in a row (#376)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 17s
- Create a `/multidrop` command
  - This will take the price of 10 drops from you and give you 11 cards to sort through
  - You then have a choice to keep the card or sacrifice it
- Create the `multidrop keep` and `multidrop sacrifice` button events

#262

Reviewed-on: #376
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-10-12 17:30:20 +01:00
f4c02d3613 Update the User entity to be nullable (#371)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 17s
- Update the user entity to be nullable in the typescript side
- The migration script already did this, but if you have the `DB_SYNC` environment variable sync it then it doesn't get set to nullable

#353

Reviewed-on: #371
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-10-12 17:26:13 +01:00
480e496984 Update dependency typescript to v5.6.2 (#375)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 17s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [typescript](https://www.typescriptlang.org/) ([source](https://github.com/microsoft/TypeScript)) | devDependencies | minor | [`5.5.4` -> `5.6.2`](https://renovatebot.com/diffs/npm/typescript/5.5.4/5.6.2) |

---

### Release Notes

<details>
<summary>microsoft/TypeScript (typescript)</summary>

### [`v5.6.2`](https://github.com/microsoft/TypeScript/releases/tag/v5.6.2): TypeScript 5.6

[Compare Source](https://github.com/microsoft/TypeScript/compare/v5.5.4...v5.6.2)

For release notes, check out the [release announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/).

For the complete list of fixed issues, check out the

-   [fixed issues query for Typescript 5.6.0 (Beta)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.0%22+is%3Aclosed+).
-   [fixed issues query for Typescript 5.6.1 (RC)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.1%22+is%3Aclosed+).
-   [fixed issues query for Typescript 5.6.2 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=milestone%3A%22TypeScript+5.6.2%22+is%3Aclosed+).

Downloads are available on:

-   [npm](https://www.npmjs.com/package/typescript)
-   [NuGet package](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #375
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-08 19:36:36 +01:00
a8a5e39e01 Update appleboy/ssh-action action to v1.1.0 (#374)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 17s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [appleboy/ssh-action](https://github.com/appleboy/ssh-action) | action | minor | `v1.0.3` -> `v1.1.0` |

---

### Release Notes

<details>
<summary>appleboy/ssh-action (appleboy/ssh-action)</summary>

### [`v1.1.0`](https://github.com/appleboy/ssh-action/releases/tag/v1.1.0)

[Compare Source](https://github.com/appleboy/ssh-action/compare/v1.0.3...v1.1.0)

##### Changelog

##### Bug fixes

-   [`0c7561b`](0c7561b1a3): fix: switch to SSH key authentication for security ([@&#8203;appleboy](https://github.com/appleboy))

##### Enhancements

-   [`9b978f0`](9b978f09f2): chore: update SSH action version in README files ([@&#8203;appleboy](https://github.com/appleboy))
-   [`1991c55`](1991c553ec): chore(file): update target file ([@&#8203;appleboy](https://github.com/appleboy))
-   [`aa293c2`](aa293c24bb): chore: optimize system configuration and API integration ([@&#8203;appleboy](https://github.com/appleboy))
-   [`036cad7`](036cad7df7): chore: update drone-ssh to version 1.7.7 ([@&#8203;appleboy](https://github.com/appleboy))
-   [`8b60782`](8b6078208d): chore(cli): enhance version handling and testing mechanisms ([@&#8203;appleboy](https://github.com/appleboy))
-   [`5ade826`](5ade826485): chore: improve CLI reliability and version visibility ([@&#8203;appleboy](https://github.com/appleboy))
-   [`eaeb069`](eaeb06998d): chore(cli): enhance curl command with silent mode and redirects ([@&#8203;appleboy](https://github.com/appleboy))
-   [`58164d0`](58164d0dc2): chore: remove Dockerfile and related configurations ([@&#8203;appleboy](https://github.com/appleboy))

##### Refactor

-   [`da612c8`](da612c8015): refactor: optimize CI pipeline for faster execution ([@&#8203;appleboy](https://github.com/appleboy))

##### Build process updates

-   [`c781418`](c78141851a): ci: enhance GitHub Actions for IPv6 and flexibility ([#&#8203;303](https://github.com/appleboy/ssh-action/issues/303)) ([@&#8203;appleboy](https://github.com/appleboy))
-   [`d2d6858`](d2d6858859): ci: enhance GitHub workflow for SSH actions and deployments ([@&#8203;appleboy](https://github.com/appleboy))
-   [`551964e`](551964ebda): ci: optimize GitHub Actions workflow configuration ([@&#8203;appleboy](https://github.com/appleboy))
-   [`f916346`](f916346256): ci: refactor CI workflow and improve robustness ([#&#8203;320](https://github.com/appleboy/ssh-action/issues/320)) ([@&#8203;appleboy](https://github.com/appleboy))
-   [`aabaf12`](aabaf1254d): ci: add bug report template ([@&#8203;appleboy](https://github.com/appleboy))
-   [`c8594ae`](c8594ae37d): ci: implement GitHub Actions for remote SSH execution ([@&#8203;appleboy](https://github.com/appleboy))
-   [`40aad53`](40aad53c5a): ci: add SSH authentication setup for GitHub Actions ([@&#8203;appleboy](https://github.com/appleboy))
-   [`0b0e770`](0b0e77098a): ci: optimize and enhance SSH server workflow ([@&#8203;appleboy](https://github.com/appleboy))
-   [`977b74a`](977b74a12d): ci: enhance CI workflow with SSH job and optimizations ([@&#8203;appleboy](https://github.com/appleboy))
-   [`2eeab5b`](2eeab5bdba): ci: refactor GitHub Actions key management ([@&#8203;appleboy](https://github.com/appleboy))
-   [`acd41e5`](acd41e5091): ci: enhance SSH job testing with varied key/password scenarios ([@&#8203;appleboy](https://github.com/appleboy))
-   [`f05aefe`](f05aefe351): ci: enhance SSH action configuration and error handling ([@&#8203;appleboy](https://github.com/appleboy))
-   [`e40b597`](e40b597081): ci: add GitHub Actions job for SSH key passphrase support ([@&#8203;appleboy](https://github.com/appleboy))
-   [`15b64dc`](15b64dc891): ci: enhance CI pipeline with SSH key handling improvements ([@&#8203;appleboy](https://github.com/appleboy))
-   [`a39b3cc`](a39b3cce7d): ci: enhance CI/CD pipeline with SSH command execution ([@&#8203;appleboy](https://github.com/appleboy))
-   [`815c574`](815c5743ac): ci: enhance deployment with multi-host SSH action ([@&#8203;appleboy](https://github.com/appleboy))
-   [`378323e`](378323e4c8): ci: add multi-server support to CI workflow ([@&#8203;appleboy](https://github.com/appleboy))
-   [`fc1c1fc`](fc1c1fce51): ci: add GitHub Actions job for [`ed25519`](https://github.com/appleboy/ssh-action/commit/ed25519) key support ([@&#8203;appleboy](https://github.com/appleboy))
-   [`f0e5a23`](f0e5a23d53): ci: add environment variable handling in GitHub Actions ([@&#8203;appleboy](https://github.com/appleboy))
-   [`9c32aa6`](9c32aa61f8): ci: refactor workflows and API integrations ([@&#8203;appleboy](https://github.com/appleboy))
-   [`97f8d75`](97f8d752b5): ci: enable root access in CI pipeline ([@&#8203;appleboy](https://github.com/appleboy))
-   [`5a8776f`](5a8776fd15): ci: switch to password authentication in GitHub workflow ([@&#8203;appleboy](https://github.com/appleboy))
-   [`b6941ae`](b6941ae5d5): ci: refactor codebase and optimize performance ([@&#8203;appleboy](https://github.com/appleboy))
-   [`43895f2`](43895f2cd5): ci: refactor SSH testing workflows and job configurations ([@&#8203;appleboy](https://github.com/appleboy))
-   [`06fa62e`](06fa62e61c): ci: rename the workflow files. ([@&#8203;appleboy](https://github.com/appleboy))
-   [`b4a07ca`](b4a07ca594): ci: enhance GitHub Actions with secret variable support ([#&#8203;330](https://github.com/appleboy/ssh-action/issues/330)) ([@&#8203;appleboy](https://github.com/appleboy))
-   [`25ce8cb`](25ce8cbbcb): ci: implement automated release workflow with GoReleaser ([@&#8203;appleboy](https://github.com/appleboy))

##### Documentation updates

-   [`8a779a5`](8a779a5b1a): docs: describe true usage of allenvs parameter ([#&#8203;301](https://github.com/appleboy/ssh-action/issues/301)) ([@&#8203;hussu010](https://github.com/hussu010))
-   [`fe44be0`](fe44be0b96): docs: improve documentation and CI robustness ([@&#8203;appleboy](https://github.com/appleboy))
-   [`dd0f09c`](dd0f09ca07): docs: improve README clarity and completeness ([@&#8203;appleboy](https://github.com/appleboy))
-   [`71d43ea`](71d43ea0f7): docs: improve documentation and testing configurations ([@&#8203;appleboy](https://github.com/appleboy))
-   [`28428a1`](28428a13f5): docs: improve cross-platform clipboard support for key copying ([@&#8203;appleboy](https://github.com/appleboy))
-   [`d732991`](d732991ab0): docs(lang): README.zh-cn Document ([#&#8203;332](https://github.com/appleboy/ssh-action/issues/332)) ([@&#8203;astralwaveio](https://github.com/astralwaveio))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #374
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-08 19:35:21 +01:00
816e550c84 Update dependency express to v4.21.0 (#373)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [express](http://expressjs.com/) ([source](https://github.com/expressjs/express)) | dependencies | minor | [`4.19.2` -> `4.21.0`](https://renovatebot.com/diffs/npm/express/4.19.2/4.21.0) |

---

### Release Notes

<details>
<summary>expressjs/express (express)</summary>

### [`v4.21.0`](https://github.com/expressjs/express/releases/tag/4.21.0)

[Compare Source](https://github.com/expressjs/express/compare/4.20.0...4.21.0)

#### What's Changed

-   Deprecate `"back"` magic string in redirects by [@&#8203;blakeembrey](https://github.com/blakeembrey) in https://github.com/expressjs/express/pull/5935
-   finalhandler@1.3.1 by [@&#8203;wesleytodd](https://github.com/wesleytodd) in https://github.com/expressjs/express/pull/5954
-   fix(deps): serve-static@1.16.2 by [@&#8203;wesleytodd](https://github.com/wesleytodd) in https://github.com/expressjs/express/pull/5951
-   Upgraded dependency qs to 6.13.0 to match qs in body-parser by [@&#8203;agadzinski93](https://github.com/agadzinski93) in https://github.com/expressjs/express/pull/5946

#### New Contributors

-   [@&#8203;agadzinski93](https://github.com/agadzinski93) made their first contribution in https://github.com/expressjs/express/pull/5946

**Full Changelog**: https://github.com/expressjs/express/compare/4.20.0...4.21.0

### [`v4.20.0`](https://github.com/expressjs/express/blob/HEAD/History.md#4200--2024-09-10)

[Compare Source](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

\==========

-   deps: serve-static@0.16.0
    -   Remove link renderization in html while redirecting
-   deps: send@0.19.0
    -   Remove link renderization in html while redirecting
-   deps: body-parser@0.6.0
    -   add `depth` option to customize the depth level in the parser
    -   IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`)
-   Remove link renderization in html while using `res.redirect`
-   deps: path-to-regexp@0.1.10
    -   Adds support for named matching groups in the routes using a regex
    -   Adds backtracking protection to parameters without regexes defined
-   deps: encodeurl@~2.0.0
    -   Removes encoding of `\`, `|`, and `^` to align better with URL spec
-   Deprecate passing `options.maxAge` and `options.expires` to `res.clearCookie`
    -   Will be ignored in v5, clearCookie will set a cookie with an expires in the past to instruct clients to delete the cookie

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #373
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-08 19:34:18 +01:00
cd7e0945a9 Update dependency @types/node to v20.16.10 (#372)
All checks were successful
Deploy To Stage / build (push) Successful in 17s
Deploy To Stage / deploy (push) Successful in 17s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.16.5` -> `20.16.10`](https://renovatebot.com/diffs/npm/@types%2fnode/20.16.5/20.16.10) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #372
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-10-08 19:32:47 +01:00
768f64b5ee Update dependency eslint to v8.57.1 (#366)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [eslint](https://eslint.org) ([source](https://github.com/eslint/eslint)) | devDependencies | patch | [`8.57.0` -> `8.57.1`](https://renovatebot.com/diffs/npm/eslint/8.57.0/8.57.1) |

---

### Release Notes

<details>
<summary>eslint/eslint (eslint)</summary>

### [`v8.57.1`](https://github.com/eslint/eslint/releases/tag/v8.57.1)

[Compare Source](https://github.com/eslint/eslint/compare/v8.57.0...v8.57.1)

#### Bug Fixes

-   [`a19072f`](a19072f9f1) fix: add logic to handle fixTypes in the lintText() method ([#&#8203;18900](https://github.com/eslint/eslint/issues/18900)) (Francesco Trotta)
-   [`04c7188`](04c718865b) fix: Don't lint same file multiple times ([#&#8203;18899](https://github.com/eslint/eslint/issues/18899)) (Francesco Trotta)
-   [`87ec3c4`](87ec3c49dd) fix: do not throw when defining a global named `__defineSetter__` ([#&#8203;18898](https://github.com/eslint/eslint/issues/18898)) (Francesco Trotta)
-   [`60a1267`](60a1267687) fix: Provide helpful error message for nullish configs ([#&#8203;18889](https://github.com/eslint/eslint/issues/18889)) (Milos Djermanovic)
-   [`a0dea8e`](a0dea8ee01) fix: allow `name` in global ignores, fix `--no-ignore` for non-global ([#&#8203;18875](https://github.com/eslint/eslint/issues/18875)) (Milos Djermanovic)
-   [`3836bb4`](3836bb48d3) fix: do not crash on error in `fs.walk` filter ([#&#8203;18886](https://github.com/eslint/eslint/issues/18886)) (Milos Djermanovic)
-   [`2dec349`](2dec349199) fix: skip processor code blocks that match only universal patterns ([#&#8203;18880](https://github.com/eslint/eslint/issues/18880)) (Milos Djermanovic)

#### Documentation

-   [`6a5add4`](6a5add41e8) docs: v8.x Add EOL banner ([#&#8203;18744](https://github.com/eslint/eslint/issues/18744)) (Amaresh  S M)
-   [`b034575`](b034575978) docs: v8.x add version support page to the dropdown ([#&#8203;18731](https://github.com/eslint/eslint/issues/18731)) (Amaresh  S M)
-   [`760ef7d`](760ef7d9db) docs: v8.x add version support page in the side navbar ([#&#8203;18740](https://github.com/eslint/eslint/issues/18740)) (Amaresh  S M)
-   [`428b7ea`](428b7ea0a9) docs: Add Powered by Algolia label to the search ([#&#8203;18658](https://github.com/eslint/eslint/issues/18658)) (Amaresh  S M)
-   [`c68c07f`](c68c07ff44) docs: version selectors synchronization ([#&#8203;18265](https://github.com/eslint/eslint/issues/18265)) (Milos Djermanovic)

#### Build Related

-   [`35d366a`](35d366aed6) build: Support updates to previous major versions ([#&#8203;18870](https://github.com/eslint/eslint/issues/18870)) (Milos Djermanovic)

#### Chores

-   [`140ec45`](140ec4569f) chore: upgrade [@&#8203;eslint/js](https://github.com/eslint/js)[@&#8203;8](https://github.com/8).57.1 ([#&#8203;18913](https://github.com/eslint/eslint/issues/18913)) (Milos Djermanovic)
-   [`bcdfc04`](bcdfc04a69) chore: package.json update for [@&#8203;eslint/js](https://github.com/eslint/js) release (Jenkins)
-   [`3f6ce8d`](3f6ce8d6b7) chore: pin vite-plugin-commonjs@0.10.1 ([#&#8203;18910](https://github.com/eslint/eslint/issues/18910)) (Milos Djermanovic)
-   [`9f07549`](9f07549795) chore: ignore `/docs/v8.x` in link checker ([#&#8203;18660](https://github.com/eslint/eslint/issues/18660)) (Milos Djermanovic)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #366
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-23 18:33:07 +01:00
1762b525b2 Add dropdown to /inventory command for quick navigation (#365)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
- Add ability to handle dropdown menus with the bot
- Add a dropdown to the inventoryhelper
- Add handler for the dropdown to navigate to that page

#344

Reviewed-on: #365
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-09-21 18:09:24 +01:00
5ebc5ff27c Update dependency body-parser to v1.20.3 (#362)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [body-parser](https://github.com/expressjs/body-parser) | dependencies | patch | [`1.20.2` -> `1.20.3`](https://renovatebot.com/diffs/npm/body-parser/1.20.2/1.20.3) |

---

### Release Notes

<details>
<summary>expressjs/body-parser (body-parser)</summary>

### [`v1.20.3`](https://github.com/expressjs/body-parser/blob/HEAD/HISTORY.md#1203--2024-09-10)

[Compare Source](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

\===================

-   deps: qs@6.13.0
-   add `depth` option to customize the depth level in the parser
-   IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #362
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-16 18:50:58 +01:00
2263871b3b Update dependency @types/jest to v29.5.13 (#361)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest)) | dependencies | patch | [`29.5.12` -> `29.5.13`](https://renovatebot.com/diffs/npm/@types%2fjest/29.5.12/29.5.13) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguODAuMCIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AiLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: #361
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-16 18:49:24 +01:00
c0e9378813 Add ability to sacrifice multiple cards at once (#354)
All checks were successful
Deploy To Stage / build (push) Successful in 14s
Deploy To Stage / deploy (push) Successful in 16s
- The `/sacrifice` command now accepts an optional parameter to be able to specify the quantity
- The `Sacrifice` button event now accepts a parameter to specify the quantity, this one is required but the command will default it to 1 if not supplied

#337

Reviewed-on: #354
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-09-15 16:57:25 +01:00
52c93c7803 Ability to trade multiple cards at once (#360)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 18s
- Add the ability to trade multiple cards at once using an optional quantity field

#338

Reviewed-on: #360
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-09-15 16:55:18 +01:00
8683c1e58a Update dependency @discordjs/rest to v2.4.0 (#357)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@discordjs/rest](https://discord.js.org) ([source](https://github.com/discordjs/discord.js/tree/HEAD/packages/rest)) | dependencies | minor | [`2.3.0` -> `2.4.0`](https://renovatebot.com/diffs/npm/@discordjs%2frest/2.3.0/2.4.0) |

---

### Release Notes

<details>
<summary>discordjs/discord.js (@&#8203;discordjs/rest)</summary>

### [`v2.4.0`](https://github.com/discordjs/discord.js/blob/HEAD/packages/rest/CHANGELOG.md#discordjsrest240---2024-09-01)

[Compare Source](https://github.com/discordjs/discord.js/compare/@discordjs/rest@2.3.0...@discordjs/rest@2.4.0)

#### Bug Fixes

-   Correct base path for GIF stickers ([#&#8203;10330](https://github.com/discordjs/discord.js/issues/10330)) ([599ad3e](599ad3eab5))

#### Features

-   **User:** Add `avatarDecorationData` ([#&#8203;9888](https://github.com/discordjs/discord.js/issues/9888)) ([3b5c600](3b5c600b9e))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #357
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-11 17:56:29 +01:00
ea0ca17044 Update dependency @types/node to v20.16.5 (#356)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.16.3` -> `20.16.5`](https://renovatebot.com/diffs/npm/@types%2fnode/20.16.3/20.16.5) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #356
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-11 17:54:46 +01:00
21d11afd31 Update dependency winston to v3.14.2 (#352)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 14s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [winston](https://github.com/winstonjs/winston) | dependencies | minor | [`3.13.1` -> `3.14.2`](https://renovatebot.com/diffs/npm/winston/3.13.1/3.14.2) |

---

### Release Notes

<details>
<summary>winstonjs/winston (winston)</summary>

### [`v3.14.2`](https://github.com/winstonjs/winston/releases/tag/v3.14.2)

[Compare Source](https://github.com/winstonjs/winston/compare/v3.14.1...v3.14.2)

-   Move initialization to constructor ([#&#8203;2503](https://github.com/winstonjs/winston/issues/2503))  [`2458ba6`](https://github.com/winstonjs/winston/commit/2458ba6)

### [`v3.14.1`](https://github.com/winstonjs/winston/releases/tag/v3.14.1)

[Compare Source](https://github.com/winstonjs/winston/compare/v3.14.0...v3.14.1)

-   Save a reference to console methods in console transport ([#&#8203;2498](https://github.com/winstonjs/winston/issues/2498))  [`e82752f`](https://github.com/winstonjs/winston/commit/e82752f)
-   Add `forceConsole` to `ConsoleTransportOptions` ([#&#8203;2496](https://github.com/winstonjs/winston/issues/2496))  [`4ff0538`](https://github.com/winstonjs/winston/commit/4ff0538)
-   Bump mocha from 10.6.0 to 10.7.0 ([#&#8203;2489](https://github.com/winstonjs/winston/issues/2489))  [`62acaad`](https://github.com/winstonjs/winston/commit/62acaad)
-   Update readme with forceConsole info ([#&#8203;2493](https://github.com/winstonjs/winston/issues/2493))  [`b2b79af`](https://github.com/winstonjs/winston/commit/b2b79af)

### [`v3.14.0`](https://github.com/winstonjs/winston/releases/tag/v3.14.0)

[Compare Source](https://github.com/winstonjs/winston/compare/v3.13.1...v3.14.0)

-   Add option forceConsole ([#&#8203;2276](https://github.com/winstonjs/winston/issues/2276))  [`b2098fd`](https://github.com/winstonjs/winston/commit/b2098fd)
-   Set \_rotate false on emit 'rotate' false ([#&#8203;2457](https://github.com/winstonjs/winston/issues/2457))  [`1719275`](https://github.com/winstonjs/winston/commit/1719275)
-   Bump [@&#8203;babel/core](https://github.com/babel/core) from 7.24.7 to 7.24.9 ([#&#8203;2485](https://github.com/winstonjs/winston/issues/2485))  [`d2859f3`](https://github.com/winstonjs/winston/commit/d2859f3)
-   Bump [@&#8203;babel/preset-env](https://github.com/babel/preset-env) from 7.24.7 to 7.24.8 ([#&#8203;2487](https://github.com/winstonjs/winston/issues/2487))  [`71e4bb1`](https://github.com/winstonjs/winston/commit/71e4bb1)
-   fix: readme ([#&#8203;2488](https://github.com/winstonjs/winston/issues/2488))  [`0cb8c7c`](https://github.com/winstonjs/winston/commit/0cb8c7c)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #352
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-02 19:12:02 +01:00
a7b03d0355 Update dependency @types/node to v20.16.3 (#351)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.16.1` -> `20.16.3`](https://renovatebot.com/diffs/npm/@types%2fnode/20.16.1/20.16.3) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #351
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-09-02 19:10:47 +01:00
761c58fb10 Upgrade package glob to v11 (#331)
All checks were successful
Deploy To Stage / build (push) Successful in 11s
Deploy To Stage / deploy (push) Successful in 16s
#311

Reviewed-on: #331
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-31 17:59:56 +01:00
711d36698b Merge branch 'main' into develop
All checks were successful
Deploy To Stage / build (push) Successful in 15s
Deploy To Stage / deploy (push) Successful in 16s
2024-08-31 13:37:28 +01:00
57e06be9af Update dependency ts-jest to v29.2.5 (#347)
All checks were successful
Deploy To Stage / build (push) Successful in 11s
Deploy To Stage / deploy (push) Successful in 14s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ts-jest](https://kulshekhar.github.io/ts-jest) ([source](https://github.com/kulshekhar/ts-jest)) | dependencies | patch | [`29.2.4` -> `29.2.5`](https://renovatebot.com/diffs/npm/ts-jest/29.2.4/29.2.5) |

---

### Release Notes

<details>
<summary>kulshekhar/ts-jest (ts-jest)</summary>

### [`v29.2.5`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#2925-2024-08-23)

[Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.2.4...v29.2.5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #347
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-26 10:08:20 +01:00
d605eb5d59 Update dependency @types/node to v20.16.1 (#346)
All checks were successful
Deploy To Stage / build (push) Successful in 11s
Deploy To Stage / deploy (push) Successful in 17s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.16.0` -> `20.16.1`](https://renovatebot.com/diffs/npm/@types%2fnode/20.16.0/20.16.1) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #346
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-26 10:07:02 +01:00
66243e6742 Fix fuzzy /view to be consistent with its pages (#345)
All checks were successful
Deploy To Stage / build (push) Successful in 15s
Deploy To Stage / deploy (push) Successful in 16s
- Fix the `/view` command to have a set amount of cards in its page rotation
- This is done now by updating it so instead of requerying each page turn it instead queries once at the start and passes the top 5 results to the button event

#154

Reviewed-on: #345
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-24 17:26:26 +01:00
79a4d18df3 Update dependency typescript to v5.5.4 (#343)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 15s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [typescript](https://www.typescriptlang.org/) ([source](https://github.com/Microsoft/TypeScript)) | devDependencies | minor | [`5.4.5` -> `5.5.4`](https://renovatebot.com/diffs/npm/typescript/5.4.5/5.5.4) |

---

### Release Notes

<details>
<summary>Microsoft/TypeScript (typescript)</summary>

### [`v5.5.4`](https://github.com/microsoft/TypeScript/releases/tag/v5.5.4): TypeScript 5.5.4

[Compare Source](https://github.com/Microsoft/TypeScript/compare/v5.5.3...v5.5.4)

For release notes, check out the [release announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/).

For the complete list of fixed issues, check out the

-   [fixed issues query for TypeScript v5.5.4 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.4%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.3 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.3%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.2 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.2%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.1 (RC)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.1%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.0 (Beta)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.0%22+is%3Aclosed+).

Downloads are available on:

-   [npm](https://www.npmjs.com/package/typescript)
-   [NuGet package](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild) (soon!)

### [`v5.5.3`](https://github.com/microsoft/TypeScript/releases/tag/v5.5.3): TypeScript 5.5.3

[Compare Source](https://github.com/Microsoft/TypeScript/compare/v5.5.2...v5.5.3)

For release notes, check out the [release announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/).

For the complete list of fixed issues, check out the

-   [fixed issues query for TypeScript v5.5.3 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.3%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.2 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.2%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.1 (RC)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.1%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.0 (Beta)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.0%22+is%3Aclosed+).

Downloads are available on:

-   [npm](https://www.npmjs.com/package/typescript)
-   [NuGet package](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild)

### [`v5.5.2`](https://github.com/microsoft/TypeScript/releases/tag/v5.5.2): TypeScript 5.5

[Compare Source](https://github.com/Microsoft/TypeScript/compare/v5.4.5...v5.5.2)

For release notes, check out the [release announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/).

For the complete list of fixed issues, check out the

-   [fixed issues query for TypeScript v5.5.2 (Stable)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.2%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.1 (RC)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.1%22+is%3Aclosed+).
-   [fixed issues query for TypeScript v5.5.0 (Beta)](https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93\&q=is%3Aissue+milestone%3A%22TypeScript+5.5.0%22+is%3Aclosed+).

Downloads are available on:

-   [npm](https://www.npmjs.com/package/typescript)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #343
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-20 17:38:06 +01:00
8e4597512f Update dependency @types/node to v20.16.0 (#342)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | minor | [`20.14.15` -> `20.16.0`](https://renovatebot.com/diffs/npm/@types%2fnode/20.14.15/20.16.0) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #342
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-20 17:37:05 +01:00
805dd00357 Upgrade node in workflow to use v20 (#341)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 14s
- Update the deployment workflows to use node v20

#330

Reviewed-on: #341
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-17 17:29:17 +01:00
5defb682c1 Update view command to use fuzzy search instead of direct card number (#340)
All checks were successful
Deploy To Stage / build (push) Successful in 13s
Deploy To Stage / deploy (push) Successful in 16s
- Install `fuse.js` package to allow for fuzzy finding
- Update the `/view` command to use fuzzy search by name instead of the card number
- Add pagination for the command via the `View` button event

#154

Reviewed-on: #340
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-17 17:26:31 +01:00
8bd5f44524 Update dependency ts-jest to v29.2.4 (#336)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 17s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ts-jest](https://kulshekhar.github.io/ts-jest) ([source](https://github.com/kulshekhar/ts-jest)) | dependencies | minor | [`29.1.5` -> `29.2.4`](https://renovatebot.com/diffs/npm/ts-jest/29.1.5/29.2.4) |

---

### Release Notes

<details>
<summary>kulshekhar/ts-jest (ts-jest)</summary>

### [`v29.2.4`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#2924-2024-08-01)

[Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.2.3...v29.2.4)

### [`v29.2.3`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#2923-2024-07-18)

[Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.2.2...v29.2.3)

### [`v29.2.2`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#2922-2024-07-10)

[Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.2.1...v29.2.2)

### [`v29.2.1`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#2921-2024-07-10)

[Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.2.0...v29.2.1)

### [`v29.2.0`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#2920-2024-07-08)

[Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.1.5...v29.2.0)

##### Bug Fixes

-   fix: don't show warning message with Node16/NodeNext ([99c4f49](https://github.com/kulshekhar/ts-jest/commit/99c4f49)), closes [#&#8203;4266](https://github.com/kulshekhar/ts-jest/issues/4266)

##### Features

-   feat(cli): allow migrating cjs `presets` to `transform` config ([22fb027](https://github.com/kulshekhar/ts-jest/commit/22fb027))
-   feat(presets): add util functions to create ESM presets ([06f78ed](https://github.com/kulshekhar/ts-jest/commit/06f78ed)), close [#&#8203;4200](https://github.com/kulshekhar/ts-jest/issues/4200)
-   feat(presets): add util functions to create CJS presets ([f9cc3c0](https://github.com/kulshekhar/ts-jest/commit/f9cc3c0)), close [#&#8203;4200](https://github.com/kulshekhar/ts-jest/issues/4200)

##### Code refactoring

-   refactor: replace lodash deps with native js implementation ([40f1708](https://github.com/kulshekhar/ts-jest/commit/40f1708))
-   refactor: use `TsJestTransformerOptions` type everywhere possibly ([7d001be](https://github.com/kulshekhar/ts-jest/commit/7d001be))
-   refactor(cli): use new preset util functions to initialize test config ([c2b56ca](https://github.com/kulshekhar/ts-jest/commit/c2b56ca))
-   refactor(presets): use create preset util functions for cjs presets ([922d6d0](https://github.com/kulshekhar/ts-jest/commit/922d6d0))
-   test: switch `react-app` to use Vite ([827c8ad](https://github.com/kulshekhar/ts-jest/commit/827c8ad))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #336
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-12 21:01:21 +01:00
9302902b17 Update dependency @types/node to v20.14.15 (#335)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.14.14` -> `20.14.15`](https://renovatebot.com/diffs/npm/@types%2fnode/20.14.14/20.14.15) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #335
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-12 20:59:56 +01:00
480786a1e9 Remove dependency minimatch (#334)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 16s
- Removed the dependency minimatch from the project
- Its not actually used anywhere, I believe it wasn't removed when I was trying out glob packages prior to merging

#312

Reviewed-on: #334
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-10 16:08:45 +01:00
981cdbfdd7 Update dependency @typescript-eslint/eslint-plugin to v7.18.0 (#333)
All checks were successful
Deploy To Stage / build (push) Successful in 10s
Deploy To Stage / deploy (push) Successful in 17s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin) ([source](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)) | devDependencies | minor | [`7.17.0` -> `7.18.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/7.17.0/7.18.0) |

---

### Release Notes

<details>
<summary>typescript-eslint/typescript-eslint (@&#8203;typescript-eslint/eslint-plugin)</summary>

### [`v7.18.0`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#7180-2024-07-29)

[Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v7.17.0...v7.18.0)

##### 🩹 Fixes

-   **eslint-plugin:** \[no-unnecessary-type-assertion] prevent runtime error when asserting a variable declared in default TS lib

-   **eslint-plugin:** \[unbound-method] report on destructuring in function parameters

-   **eslint-plugin:** \[no-duplicate-type-constituents] shouldn't report on error types

-   **eslint-plugin:** \[strict-boolean-expressions] support branded booleans

##### ❤️  Thank You

-   auvred
-   Oliver Salzburg
-   Vinccool96
-   Yukihiro Hasegawa

You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #333
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-05 18:52:08 +01:00
7a99d273f6 Update dependency @types/node to v20.14.14 (#332)
All checks were successful
Deploy To Stage / build (push) Successful in 12s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.14.13` -> `20.14.14`](https://renovatebot.com/diffs/npm/@types%2fnode/20.14.13/20.14.14) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #332
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-08-05 18:51:10 +01:00
ec0292d658 Upgrade @types/uuid to v10 (#329)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
#310

Reviewed-on: #329
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-02 18:23:01 +01:00
89c6dc527a Document the google drive sync process (#328)
All checks were successful
Deploy To Stage / build (push) Successful in 16s
Deploy To Stage / deploy (push) Successful in 16s
#82

Reviewed-on: #328
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2024-08-02 18:20:23 +01:00
73776408b5 Update dependency @typescript-eslint/eslint-plugin to v7.17.0 (#326)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin) ([source](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)) | devDependencies | minor | [`7.15.0` -> `7.17.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/7.15.0/7.17.0) |

---

### Release Notes

<details>
<summary>typescript-eslint/typescript-eslint (@&#8203;typescript-eslint/eslint-plugin)</summary>

### [`v7.17.0`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#7170-2024-07-22)

[Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v7.16.1...v7.17.0)

##### 🚀 Features

-   **eslint-plugin:** backport no-unsafe-function type, no-wrapper-object-types from v8 to v7

-   **eslint-plugin:** \[return-await] add option to report in error-handling scenarios only, and deprecate "never"

##### 🩹 Fixes

-   **eslint-plugin:** \[no-floating-promises] check top-level type assertions (and more)

-   **eslint-plugin:** \[strict-boolean-expressions] consider assertion function argument a boolean context

-   **eslint-plugin:** \[no-unnecessary-condition] false positive on optional private field

##### ❤️  Thank You

-   Armano
-   Josh Goldberg 
-   Kirk Waiblinger
-   StyleShit

You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.

### [`v7.16.1`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#7161-2024-07-15)

[Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v7.16.0...v7.16.1)

##### 🩹 Fixes

-   **eslint-plugin:** \[no-unnecessary-type-parameters] descend into all parts of mapped types in no-unnecessary-type-parameters

##### ❤️  Thank You

-   Dan Vanderkam

You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.

### [`v7.16.0`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#7160-2024-07-08)

[Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v7.15.0...v7.16.0)

##### 🚀 Features

-   **rule-tester:** stricter rule test validations

-   **eslint-plugin:** \[no-unnecessary-parameter-property-assignment] add new rule

-   **eslint-plugin:** add support for nested namespaces to unsafe-member-access

-   **eslint-plugin:** \[no-floating-promises] add checkThenables option

##### 🩹 Fixes

-   **deps:** update dependency [@&#8203;eslint-community/regexpp](https://github.com/eslint-community/regexpp) to v4.11.0

-   **eslint-plugin:** \[no-floating-promises] add `suggestions` to tests from [#&#8203;9263](https://github.com/typescript-eslint/typescript-eslint/issues/9263) `checkThenables`

-   **website:** react key error on internal pages of website

-   **eslint-plugin:** \[restrict-template-expressions] don't report tuples if `allowArray` option is enabled

##### ❤️  Thank You

-   Abraham Guo
-   auvred
-   Josh Goldberg 
-   Juan Sanchez
-   Vinccool96
-   YeonJuan
-   Yukihiro Hasegawa

You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #326
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-07-29 18:19:47 +01:00
bf9b748f4f Update dependency @types/node to v20.14.13 (#325)
All checks were successful
Deploy To Stage / build (push) Successful in 9s
Deploy To Stage / deploy (push) Successful in 16s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`20.14.11` -> `20.14.13`](https://renovatebot.com/diffs/npm/@types%2fnode/20.14.11/20.14.13) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: #325
Reviewed-by: Vylpes <ethan@vylpes.com>
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
2024-07-29 18:18:11 +01:00
55 changed files with 3670 additions and 2019 deletions

View file

@ -32,9 +32,8 @@ DB_AUTH_PASS=
DB_SYNC=
DB_LOGGING=
DB_DATA_LOCATION=./.temp/database
DB_CARD_FILE=:memory:
DB_ROOT_HOST=0.0.0.0
EXPRESS_PORT=3302
GDRIVESYNC_AUTO=true
GDRIVESYNC_AUTO=false

View file

@ -1,45 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
},
"globals": {
"jest": true,
"require": true,
"exports": true,
"process": true
},
"ignorePatterns": [
"dist/**/*"
]
}

View file

@ -30,7 +30,7 @@ jobs:
needs: build
runs-on: node
steps:
- uses: https://github.com/appleboy/ssh-action@v1.0.3
- uses: https://github.com/appleboy/ssh-action@v1.1.0
env:
DB_NAME: ${{ secrets.PROD_DB_NAME }}
DB_AUTH_USER: ${{ secrets.PROD_DB_AUTH_USER }}

View file

@ -30,7 +30,7 @@ jobs:
needs: build
runs-on: node
steps:
- uses: https://github.com/appleboy/ssh-action@v1.0.3
- uses: https://github.com/appleboy/ssh-action@v1.1.0
env:
DB_NAME: ${{ secrets.STAGE_DB_NAME }}
DB_AUTH_USER: ${{ secrets.STAGE_DB_AUTH_USER }}

View file

@ -18,7 +18,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test

View file

@ -0,0 +1 @@
DROP TABLE `user_effect`;

View file

@ -0,0 +1,10 @@
CREATE TABLE `user_effect` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`Name` varchar(255) NOT NULL,
`UserId` varchar(255) NOT NULL,
`Unused` int NOT NULL DEFAULT 0,
`WhenExpires` datetime NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

35
docs/google-drive-sync.md Normal file
View file

@ -0,0 +1,35 @@
# Google Drive Sync
The bot relies on an external sync between the local file system and Google
Drive in order to get newer cards to the bot. This is done using
[Rclone](https://rclone.org/).
The process for this is done by once the `/gdrivesync` command is executed by
an admin user of the bot, which calls the system shell to run rclone to the
card folder.
- The admins who can run the command is specifed in `$BOT_ADMINS`, which are
discord user ids separated by commas.
- The card folder is located at `$DATA_DIR/cards`.
- The source requires rclone's remote to be setup as `card-drop-gdrive`.
The exact command it runs is: `rclone sync card-drop-gdrive: $DATA_DIR/cards`.
Once it syncs the database will reread all the cards for updates and then load
them into the bot to be given.
## Safe Mode
Safe mode is a function of the bot which disables the `/drop` command function
and any other functions which rely on the card metadata. Safe mode is activated
upon failure to sync properly. It is disabled once errors are resolved.
The reason for safe mode is to ensure that the bot stays online for admins to
be able to resync the bot in case there's an error without it crashing.
## Google Drive
Please see the Rclone documentation on how to setup a remote using Google
Drive. You will need to make an app password for this.
- scope: `drive.readonly`
- root\_folder\_id: The folder id where the cards are located, this can be found
by looking at the url when viewing the folder in the browser in google drive.

55
eslint.config.mjs Normal file
View file

@ -0,0 +1,55 @@
import js from "@eslint/js";
import ts from "typescript-eslint";
export default [
{
ignores: [
"**/dist/",
"eslint.config.mjs",
"jest.config.cjs",
"jest.setup.js",
"**/.temp/**/*"
],
},
js.configs.recommended,
...ts.configs.recommended,
{
languageOptions: {
globals: {
exports: "writable",
module: "writable",
require: "writable",
process: "writable",
console: "writable",
jest: "writable",
},
ecmaVersion: 6,
sourceType: "script",
},
files: [
"./src",
"./tests"
],
rules: {
camelcase: "error",
"brace-style": ["error", "1tbs"],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", {
before: false,
after: true,
}],
"comma-style": ["error", "last"],
"arrow-body-style": ["error", "as-needed"],
"arrow-parens": ["error", "as-needed"],
"arrow-spacing": "error",
"no-var": "error",
"prefer-template": "error",
"prefer-const": "error",
},
}
];

View file

@ -1,3 +1,4 @@
jest.setTimeout(1 * 1000); // 1 second
jest.resetModules();
jest.resetAllMocks();
jest.resetAllMocks();
jest.useFakeTimers();

View file

@ -7,7 +7,7 @@
"clean": "rm -rf node_modules/ dist/",
"build": "tsc",
"start": "node ./dist/bot.js",
"test": "echo true",
"test": "jest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js",
@ -27,37 +27,36 @@
"dependencies": {
"@discordjs/rest": "^2.0.0",
"@types/clone-deep": "^4.0.4",
"@types/express": "^4.17.20",
"@types/jest": "^29.0.0",
"@types/uuid": "^9.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/uuid": "^10.0.0",
"body-parser": "^1.20.2",
"canvas": "^2.11.2",
"clone-deep": "^4.0.1",
"cron": "^3.1.7",
"discord.js": "^14.15.3",
"discord.js": "^14.16.3",
"dotenv": "^16.0.0",
"express": "^4.18.2",
"glob": "^10.3.10",
"fuse.js": "^7.0.0",
"glob": "^11.0.0",
"jest": "^29.0.0",
"jest-mock-extended": "^3.0.0",
"jimp": "^0.22.12",
"minimatch": "9.0.5",
"jimp": "^1.6.0",
"mysql": "^2.18.1",
"ts-jest": "^29.0.0",
"typeorm": "0.3.20",
"winston": "^3.11.0",
"winston": "^3.15.0",
"winston-daily-rotate-file": "^5.0.0",
"winston-discord-transport": "^1.3.0"
},
"resolutions": {
"**/ws": "^8.17.1"
},
"resolutions": {},
"devDependencies": {
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.16.0",
"eslint": "^8.56.0",
"np": "^9.0.0",
"typescript": "^5.0.0"
"@types/node": "^22.8.1",
"@typescript-eslint/eslint-plugin": "^8.11.0",
"@typescript-eslint/parser": "^8.11.0",
"eslint": "^9.13.0",
"np": "^10.0.7",
"typescript": "^5.0.0",
"typescript-eslint": "^8.11.0"
}
}

View file

@ -38,6 +38,7 @@ const client = new CoreClient([
Registry.RegisterCommands();
Registry.RegisterButtonEvents();
Registry.RegisterStringDropdownEvents();
if (!existsSync(`${process.env.DATA_DIR}/cards`) && process.env.GDRIVESYNC_AUTO && process.env.GDRIVESYNC_AUTO == "true") {
console.log("Card directory not found, syncing...");

View file

@ -12,6 +12,7 @@ export default class Claim extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
if (!interaction.guild || !interaction.guildId) return;
if (!interaction.channel) return;
if (!interaction.channel.isSendable()) return;
await interaction.deferUpdate();
@ -58,7 +59,7 @@ export default class Claim extends ButtonEvent {
if (!inventory) {
inventory = new Inventory(userId, cardNumber, 1);
} else {
inventory.SetQuantity(inventory.Quantity + 1);
inventory.AddQuantity(1);
}
await inventory.Save(Inventory, inventory);

158
src/buttonEvents/Effects.ts Normal file
View file

@ -0,0 +1,158 @@
import {ActionRowBuilder, ButtonBuilder, ButtonInteraction,ButtonStyle,Embed,EmbedBuilder} from "discord.js";
import {ButtonEvent} from "../type/buttonEvent";
import EffectHelper from "../helpers/EffectHelper";
import { EffectDetails } from "../constants/EffectDetails";
import TimeLengthInput from "../helpers/TimeLengthInput";
import EmbedColours from "../constants/EmbedColours";
export default class Effects extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
const action = interaction.customId.split(" ")[1];
switch (action) {
case "list":
await this.List(interaction);
break;
case "use":
await this.Use(interaction);
break;
}
}
private async List(interaction: ButtonInteraction) {
const pageOption = interaction.customId.split(" ")[2];
const page = Number(pageOption);
if (!page) {
await interaction.reply("Page option is not a valid number");
return;
}
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
await interaction.update({
embeds: [ result.embed ],
components: [ result.row ],
});
}
private async Use(interaction: ButtonInteraction) {
const subaction = interaction.customId.split(" ")[2];
switch (subaction) {
case "confirm":
await this.UseConfirm(interaction);
break;
case "cancel":
await this.UseCancel(interaction);
break;
}
}
private async UseConfirm(interaction: ButtonInteraction) {
const id = interaction.customId.split(" ")[3];
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
await interaction.reply("Unable to find effect!");
return;
}
const now = new Date();
const whenExpires = new Date(now.getMilliseconds() + effectDetail.duration);
const result = await EffectHelper.UseEffect(interaction.user.id, id, whenExpires);
if (result) {
const embed = new EmbedBuilder()
.setTitle("Effect Used")
.setDescription("You now have an active effect!")
.setColor(EmbedColours.Green)
.addFields([
{
name: "Effect",
value: effectDetail.friendlyName,
inline: true,
},
{
name: "Expires",
value: `<t:${whenExpires.getMilliseconds()}:f>`,
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setLabel("Confirm")
.setCustomId(`effects use confirm ${effectDetail.id}`)
.setStyle(ButtonStyle.Primary)
.setDisabled(true),
new ButtonBuilder()
.setLabel("Cancel")
.setCustomId(`effects use cancel ${effectDetail.id}`)
.setStyle(ButtonStyle.Danger)
.setDisabled(true),
]);
await interaction.update({
embeds: [ embed ],
components: [ row ],
});
return;
}
await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
}
private async UseCancel(interaction: ButtonInteraction) {
const id = interaction.customId.split(" ")[3];
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
await interaction.reply("Unable to find effect!");
return;
}
const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
const embed = new EmbedBuilder()
.setTitle("Effect Use Cancelled")
.setDescription("The effect from your inventory has not been used")
.setColor(EmbedColours.Grey)
.addFields([
{
name: "Effect",
value: effectDetail.friendlyName,
inline: true,
},
{
name: "Expires",
value: timeLengthInput.GetLengthShort(),
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setLabel("Confirm")
.setCustomId(`effects use confirm ${effectDetail.id}`)
.setStyle(ButtonStyle.Primary)
.setDisabled(true),
new ButtonBuilder()
.setLabel("Cancel")
.setCustomId(`effects use cancel ${effectDetail.id}`)
.setStyle(ButtonStyle.Danger)
.setDisabled(true),
]);
await interaction.update({
embeds: [ embed ],
components: [ row ],
});
}
}

View file

@ -11,7 +11,7 @@ export default class Inventory extends ButtonEvent {
const page = interaction.customId.split(" ")[2];
AppLogger.LogSilly("Button/Inventory", `Parameters: userid=${userid}, page=${page}`);
await interaction.deferUpdate();
const member = interaction.guild.members.cache.find(x => x.id == userid) || await interaction.guild.members.fetch(userid);
@ -34,7 +34,7 @@ export default class Inventory extends ButtonEvent {
await interaction.editReply({
files: [ embed.image ],
embeds: [ embed.embed ],
components: [ embed.row ],
components: [ embed.row1, embed.row2 ],
});
} catch (e) {
AppLogger.LogError("Button/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`);

View file

@ -0,0 +1,213 @@
import { AttachmentBuilder, ButtonInteraction, EmbedBuilder } from "discord.js";
import { ButtonEvent } from "../type/buttonEvent";
import AppLogger from "../client/appLogger";
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
import Inventory from "../database/entities/app/Inventory";
import EmbedColours from "../constants/EmbedColours";
import { readFileSync } from "fs";
import path from "path";
import ErrorMessages from "../constants/ErrorMessages";
import User from "../database/entities/app/User";
import { GetSacrificeAmount } from "../constants/CardRarity";
export default class Multidrop extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
const action = interaction.customId.split(" ")[1];
switch (action) {
case "keep":
await this.Keep(interaction);
break;
case "sacrifice":
await this.Sacrifice(interaction);
break;
default:
await interaction.reply("Invalid action");
AppLogger.LogError("Button/Multidrop", `Invalid action, ${action}`);
}
}
private async Keep(interaction: ButtonInteraction) {
const cardNumber = interaction.customId.split(" ")[2];
let cardsRemaining = Number(interaction.customId.split(" ")[3]) || 0;
const userId = interaction.customId.split(" ")[4];
if (interaction.user.id != userId) {
await interaction.reply("You're not the user this drop was made for!");
return;
}
const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!card) {
await interaction.reply("Unable to find card.");
AppLogger.LogWarn("Button/Multidrop/Keep", `Card not found, ${cardNumber}`);
return;
}
if (cardsRemaining < 0) {
await interaction.reply("Your multidrop has ran out! Please buy a new one!");
return;
}
const user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
AppLogger.LogWarn("Button/Multidrop/Keep", ErrorMessages.UnableToFetchUser);
await interaction.reply(ErrorMessages.UnableToFetchUser);
return;
}
// Claim
let inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardNumber);
if (!inventory) {
inventory = new Inventory(interaction.user.id, cardNumber, 1);
} else {
inventory.AddQuantity(1);
}
await inventory.Save(Inventory, inventory);
// Pack has ran out
if (cardsRemaining == 0) {
const embed = new EmbedBuilder()
.setDescription("Your multidrop has ran out! Please buy a new one!")
.setColor(EmbedColours.Ok);
await interaction.update({
embeds: [ embed ],
attachments: [],
components: [],
});
return;
}
// Drop next card
const randomCard = CardDropHelperMetadata.GetRandomCard();
cardsRemaining -= 1;
if (!randomCard) {
AppLogger.LogWarn("Button/Multidrop/Keep", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}
await interaction.deferUpdate();
try {
const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
const imageFileName = randomCard.card.path.split("/").pop()!;
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
const quantityClaimed = inventory ? inventory.Quantity : 0;
const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
} catch (e) {
AppLogger.LogError("Button/Multidrop/Keep", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
}
}
private async Sacrifice(interaction: ButtonInteraction) {
const cardNumber = interaction.customId.split(" ")[2];
let cardsRemaining = Number(interaction.customId.split(" ")[3]) || 0;
const userId = interaction.customId.split(" ")[4];
if (interaction.user.id != userId) {
await interaction.reply("You're not the user this drop was made for!");
return;
}
const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!card) {
await interaction.reply("Unable to find card.");
AppLogger.LogWarn("Button/Multidrop/Sacrifice", `Card not found, ${cardNumber}`);
return;
}
if (cardsRemaining < 0) {
await interaction.reply("Your multidrop has ran out! Please buy a new one!");
return;
}
const user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
AppLogger.LogWarn("Button/Multidrop/Sacrifice", ErrorMessages.UnableToFetchUser);
await interaction.reply(ErrorMessages.UnableToFetchUser);
return;
}
// Sacrifice
const sacrificeAmount = GetSacrificeAmount(card.card.type);
user.AddCurrency(sacrificeAmount);
await user.Save(User, user);
// Pack has ran out
if (cardsRemaining == 0) {
const embed = new EmbedBuilder()
.setDescription("Your multidrop has ran out! Please buy a new one!")
.setColor(EmbedColours.Ok);
await interaction.update({
embeds: [ embed ],
attachments: [],
components: [],
});
return;
}
// Drop next card
const randomCard = CardDropHelperMetadata.GetRandomCard();
cardsRemaining -= 1;
if (!randomCard) {
AppLogger.LogWarn("Button/Multidrop/Sacrifice", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}
await interaction.deferUpdate();
try {
const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
const imageFileName = randomCard.card.path.split("/").pop()!;
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
const quantityClaimed = inventory ? inventory.Quantity : 0;
const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
} catch (e) {
AppLogger.LogError("Button/Multidrop/Sacrifice", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
}
}
}

View file

@ -23,6 +23,7 @@ export default class Sacrifice extends ButtonEvent {
private async confirm(interaction: ButtonInteraction) {
const userId = interaction.customId.split(" ")[2];
const cardNumber = interaction.customId.split(" ")[3];
const quantity = Number(interaction.customId.split(" ")[4]) || 1;
if (userId != interaction.user.id) {
await interaction.reply("Only the user who created this sacrifice can confirm it.");
@ -31,11 +32,16 @@ export default class Sacrifice extends ButtonEvent {
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!cardInInventory) {
if (!cardInInventory || cardInInventory.Quantity == 0) {
await interaction.reply("Unable to find card in inventory.");
return;
}
if (cardInInventory.Quantity < quantity) {
await interaction.reply("You can only sacrifice what you own.");
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!cardData) {
@ -50,11 +56,11 @@ export default class Sacrifice extends ButtonEvent {
return;
}
cardInInventory.RemoveQuantity(1);
cardInInventory.RemoveQuantity(quantity);
await cardInInventory.Save(Inventory, cardInInventory);
const cardValue = GetSacrificeAmount(cardData.card.type);
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
const cardRarityString = CardRarityToString(cardData.card.type);
user.AddCurrency(cardValue);
@ -66,6 +72,7 @@ export default class Sacrifice extends ButtonEvent {
`Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`,
];
@ -98,6 +105,7 @@ export default class Sacrifice extends ButtonEvent {
private async cancel(interaction: ButtonInteraction) {
const userId = interaction.customId.split(" ")[2];
const cardNumber = interaction.customId.split(" ")[3];
const quantity = Number(interaction.customId.split(" ")[4]) || 1;
if (userId != interaction.user.id) {
await interaction.reply("Only the user who created this sacrifice can cancel it.");
@ -106,11 +114,16 @@ export default class Sacrifice extends ButtonEvent {
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!cardInInventory) {
if (!cardInInventory || cardInInventory.Quantity == 0) {
await interaction.reply("Unable to find card in inventory.");
return;
}
if (cardInInventory.Quantity < quantity) {
await interaction.reply("You can only sacrifice what you own.");
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!cardData) {
@ -118,7 +131,7 @@ export default class Sacrifice extends ButtonEvent {
return;
}
const cardValue = GetSacrificeAmount(cardData.card.type);
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
const cardRarityString = CardRarityToString(cardData.card.type);
const description = [
@ -126,6 +139,7 @@ export default class Sacrifice extends ButtonEvent {
`Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`,
];

View file

@ -28,8 +28,10 @@ export default class Trade extends ButtonEvent {
const user2CardNumber = interaction.customId.split(" ")[5];
const expiry = interaction.customId.split(" ")[6];
const timeoutId = interaction.customId.split(" ")[7];
const user1Quantity = Number(interaction.customId.split(" ")[8]) || 1;
const user2Quantity = Number(interaction.customId.split(" ")[9]) || 1;
AppLogger.LogSilly("Button/Trade/AcceptTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, expiry=${expiry}, timeoutId=${timeoutId}`);
AppLogger.LogSilly("Button/Trade/AcceptTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, expiry=${expiry}, timeoutId=${timeoutId} user1Quantity=${user1Quantity} user2Quantity=${user2Quantity}`);
const expiryDate = new Date(expiry);
@ -67,13 +69,13 @@ export default class Trade extends ButtonEvent {
return;
}
if (user1UserInventory1.Quantity < 1 || user2UserInventory1.Quantity < 1) {
if (user1UserInventory1.Quantity < user1Quantity || user2UserInventory1.Quantity < user2Quantity) {
await interaction.reply("One or more of the items you are trying to trade does not exist.");
return;
}
user1UserInventory1.RemoveQuantity(1);
user2UserInventory1.RemoveQuantity(1);
user1UserInventory1.RemoveQuantity(user1Quantity);
user2UserInventory1.RemoveQuantity(user2Quantity);
await user1UserInventory1.Save(Inventory, user1UserInventory1);
await user2UserInventory1.Save(Inventory, user2UserInventory1);
@ -82,15 +84,15 @@ export default class Trade extends ButtonEvent {
let user2UserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(user2UserId, user1CardNumber);
if (!user1UserInventory2) {
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, 1);
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, user2Quantity);
} else {
user1UserInventory2.AddQuantity(1);
user1UserInventory2.AddQuantity(user2Quantity);
}
if (!user2UserInventory2) {
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, 1);
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, user1Quantity);
} else {
user2UserInventory2.AddQuantity(1);
user2UserInventory2.AddQuantity(user1Quantity);
}
await user1UserInventory2.Save(Inventory, user1UserInventory2);
@ -106,12 +108,12 @@ export default class Trade extends ButtonEvent {
.addFields([
{
name: `${user1User.username} Receives`,
value: `${user2Item.id}: ${user2Item.name}`,
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`,
inline: true,
},
{
name: `${user2User.username} Receives`,
value: `${user1Item.id}: ${user1Item.name}`,
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`,
inline: true,
},
{
@ -144,6 +146,8 @@ export default class Trade extends ButtonEvent {
const user2CardNumber = interaction.customId.split(" ")[5];
// No need to get expiry date
const timeoutId = interaction.customId.split(" ")[7];
const user1Quantity = Number(interaction.customId.split(" ")[8]) || 1;
const user2Quantity = Number(interaction.customId.split(" ")[9]) || 1;
AppLogger.LogSilly("Button/Trade/DeclineTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, timeoutId=${timeoutId}`);
@ -178,12 +182,12 @@ export default class Trade extends ButtonEvent {
.addFields([
{
name: `${user1User.username} Receives`,
value: `${user2Item.id}: ${user2Item.name}`,
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`,
inline: true,
},
{
name: `${user2User.username} Receives`,
value: `${user1Item.id}: ${user1Item.name}`,
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`,
inline: true,
},
{

25
src/buttonEvents/View.ts Normal file
View file

@ -0,0 +1,25 @@
import {ButtonInteraction} from "discord.js";
import {ButtonEvent} from "../type/buttonEvent.js";
import CardSearchHelper from "../helpers/CardSearchHelper.js";
export default class View extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
const page = interaction.customId.split(" ")[1];
const results = interaction.customId.split(" ").splice(2);
await interaction.deferUpdate();
const searchResult = await CardSearchHelper.GenerateSearchPageFromQuery(results, interaction.user.id, Number(page));
if (!searchResult) {
await interaction.followUp("No results found");
return;
}
await interaction.editReply({
embeds: [ searchResult.embed ],
components: [ searchResult.row ],
files: [ searchResult.attachment ],
});
}
}

View file

@ -17,11 +17,14 @@ import AppLogger from "./appLogger";
import TimerHelper from "../helpers/TimerHelper";
import GiveCurrency from "../timers/GiveCurrency";
import PurgeClaims from "../timers/PurgeClaims";
import StringDropdownEventItem from "../contracts/StringDropdownEventItem";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
export class CoreClient extends Client {
private static _commandItems: ICommandItem[];
private static _eventExecutors: EventExecutors;
private static _buttonEvents: IButtonEventItem[];
private static _stringDropdowns: StringDropdownEventItem[];
private _events: Events;
private _util: Util;
@ -45,6 +48,10 @@ export class CoreClient extends Client {
return this._buttonEvents;
}
public static get stringDropdowns(): StringDropdownEventItem[] {
return this._stringDropdowns;
}
constructor(intents: number[]) {
super({ intents: intents });
dotenv.config();
@ -59,6 +66,7 @@ export class CoreClient extends Client {
CoreClient._commandItems = [];
CoreClient._buttonEvents = [];
CoreClient._stringDropdowns = [];
this._events = new Events();
this._util = new Util();
@ -408,4 +416,19 @@ export class CoreClient extends Client {
AppLogger.LogVerbose("Client", `Registered Button Event: ${buttonId}`);
}
}
public static RegisterStringDropdownEvent(dropdownId: string, event: StringDropdownEvent, environment: Environment = Environment.All) {
const item: StringDropdownEventItem = {
DropdownId: dropdownId,
Event: event,
Environment: environment,
};
if ((environment & CoreClient.Environment) == CoreClient.Environment) {
CoreClient._stringDropdowns.push(item);
AppLogger.LogVerbose("Client", `Registered String Dropdown Event: ${dropdownId}`);
}
}
}

View file

@ -3,6 +3,7 @@ import ChatInputCommand from "./interactionCreate/ChatInputCommand";
import Button from "./interactionCreate/Button";
import AppLogger from "./appLogger";
import NewUserDiscovery from "./interactionCreate/middleware/NewUserDiscovery";
import StringDropdown from "./interactionCreate/StringDropdown";
export class Events {
public async onInteractionCreate(interaction: Interaction) {
@ -19,6 +20,11 @@ export class Events {
AppLogger.LogVerbose("Client", `Button: ${interaction.customId}`);
Button.onButtonClicked(interaction);
}
if (interaction.isStringSelectMenu()) {
AppLogger.LogVerbose("Client", `StringDropdown: ${interaction.customId}`);
StringDropdown.onStringDropdownSelected(interaction);
}
}
// Emit when bot is logged in and ready to use

View file

@ -0,0 +1,29 @@
import {StringSelectMenuInteraction} from "discord.js";
import {CoreClient} from "../client";
import AppLogger from "../appLogger";
export default class StringDropdown {
public static async onStringDropdownSelected(interaction: StringSelectMenuInteraction) {
if (!interaction.isStringSelectMenu()) return;
const item = CoreClient.stringDropdowns.find(x => x.DropdownId == interaction.customId.split(" ")[0]);
if (!item) {
AppLogger.LogVerbose("StringDropdown", `Event not found: ${interaction.customId}`);
await interaction.reply("Event not found");
return;
}
try {
AppLogger.LogDebug("StringDropdown", `Executing ${interaction.customId}`);
item.Event.execute(interaction);
} catch (e) {
AppLogger.LogError("StringDropdown", `Error occurred while executing event: ${interaction.customId}`);
AppLogger.LogError("StringDropdown", e as string);
await interaction.reply("An error occurred while executing the event");
}
}
}

View file

@ -10,6 +10,9 @@ import path from "path";
import AppLogger from "../client/appLogger";
import User from "../database/entities/app/User";
import CardConstants from "../constants/CardConstants";
import ErrorMessages from "../constants/ErrorMessages";
import { DropResult } from "../contracts/SeriesMetadata";
import EffectHelper from "../helpers/EffectHelper";
export default class Drop extends Command {
constructor() {
@ -22,14 +25,13 @@ export default class Drop extends Command {
public override async execute(interaction: CommandInteraction) {
if (!CoreClient.AllowDrops) {
await interaction.reply("Bot is currently syncing, please wait until its done.");
await interaction.reply(ErrorMessages.BotSyncing);
return;
}
if (await Config.GetValue("safemode") == "true") {
AppLogger.LogWarn("Commands/Drop", "Safe Mode is active, refusing to send next drop.");
await interaction.reply("Safe Mode has been activated, please resync to continue.");
AppLogger.LogWarn("Commands/Drop", ErrorMessages.SafeMode);
await interaction.reply(ErrorMessages.SafeMode);
return;
}
@ -43,16 +45,23 @@ export default class Drop extends Command {
}
if (user.Currency < CardConstants.ClaimCost) {
await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`);
await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency));
return;
}
const randomCard = CardDropHelperMetadata.GetRandomCard();
let randomCard: DropResult | undefined;
const hasChanceUpEffect = await EffectHelper.HasEffect(interaction.user.id, "unclaimed");
if (hasChanceUpEffect && Math.random() <= CardConstants.UnusedChanceUpChance) {
randomCard = await CardDropHelperMetadata.GetRandomCardUnclaimed(interaction.user.id);
} else {
randomCard = CardDropHelperMetadata.GetRandomCard();
}
if (!randomCard) {
AppLogger.LogWarn("Commands/Drop", "Unable to fetch card, please try again. (randomCard is null)");
await interaction.reply("Unable to fetch card, please try again.");
AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}

116
src/commands/effects.ts Normal file
View file

@ -0,0 +1,116 @@
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder} from "discord.js";
import {Command} from "../type/command";
import EffectHelper from "../helpers/EffectHelper";
import {EffectDetails} from "../constants/EffectDetails";
import UserEffect from "../database/entities/app/UserEffect";
import TimeLengthInput from "../helpers/TimeLengthInput";
import EmbedColours from "../constants/EmbedColours";
export default class Effects extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("effects")
.setDescription("Effects")
.addSubcommand(x => x
.setName("list")
.setDescription("List all effects I have")
.addNumberOption(x => x
.setName("page")
.setDescription("The page number")
.setMinValue(1)))
.addSubcommand(x => x
.setName("use")
.setDescription("Use an effect in your inventory")
.addStringOption(y => y
.setName("id")
.setDescription("The effect id to use")
.setRequired(true)
.setChoices([
{ name: "Unclaimed Chance Up", value: "unclaimed" },
])));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case "list":
await this.List(interaction);
break;
case "use":
await this.Use(interaction);
break;
}
}
private async List(interaction: CommandInteraction) {
const pageOption = interaction.options.get("page");
const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1;
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
await interaction.reply({
embeds: [ result.embed ],
components: [ result.row ],
});
}
private async Use(interaction: CommandInteraction) {
const id = interaction.options.get("id", true).value!.toString();
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
await interaction.reply("Unable to find effect!");
return;
}
const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id);
if (!canUseEffect) {
await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
return;
}
const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
const embed = new EmbedBuilder()
.setTitle("Effect Confirmation")
.setDescription("Would you like to use this effect?")
.setColor(EmbedColours.Ok)
.addFields([
{
name: "Effect",
value: effectDetail.friendlyName,
inline: true,
},
{
name: "Length",
value: timeLengthInput.GetLengthShort(),
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setLabel("Confirm")
.setCustomId(`effects use confirm ${effectDetail.id}`)
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setLabel("Cancel")
.setCustomId(`effects use cancel ${effectDetail.id}`)
.setStyle(ButtonStyle.Danger),
]);
await interaction.reply({
embeds: [ embed ],
components: [ row ],
});
}
}

82
src/commands/id.ts Normal file
View file

@ -0,0 +1,82 @@
import { AttachmentBuilder, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import { CoreClient } from "../client/client";
import { readFileSync } from "fs";
import path from "path";
import Inventory from "../database/entities/app/Inventory";
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
import AppLogger from "../client/appLogger";
export default class Id extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("id")
.setDescription("View a specific command by its id")
.addStringOption(x =>
x
.setName("cardnumber")
.setDescription("The card number to view")
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const cardNumber = interaction.options.get("cardnumber");
AppLogger.LogSilly("Commands/View", `Parameters: cardNumber=${cardNumber?.value}`);
if (!cardNumber || !cardNumber.value) {
await interaction.reply("Card number is required.");
return;
}
const card = CoreClient.Cards
.flatMap(x => x.cards)
.find(x => x.id == cardNumber.value);
if (!card) {
await interaction.reply("Card not found.");
return;
}
const series = CoreClient.Cards
.find(x => x.cards.includes(card))!;
let image: Buffer;
const imageFileName = card.path.split("/").pop()!;
try {
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
} catch {
AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
await interaction.reply(`Unable to fetch image for card ${card.id}.`);
return;
}
await interaction.deferReply();
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id);
const quantityClaimed = inventory ? inventory.Quantity : 0;
const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
try {
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
});
} catch (e) {
AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${e}`);
if (e instanceof DiscordAPIError) {
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}.`);
} else {
await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN.");
}
}
}
}

View file

@ -47,7 +47,7 @@ export default class Inventory extends Command {
await interaction.followUp({
files: [ embed.image ],
embeds: [ embed.embed ],
components: [ embed.row ],
components: [ embed.row1, embed.row2 ],
});
} catch (e) {
AppLogger.LogError("Commands/Inventory", e as string);

87
src/commands/multidrop.ts Normal file
View file

@ -0,0 +1,87 @@
import { AttachmentBuilder, CommandInteraction, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import { CoreClient } from "../client/client";
import ErrorMessages from "../constants/ErrorMessages";
import Config from "../database/entities/app/Config";
import AppLogger from "../client/appLogger";
import User from "../database/entities/app/User";
import CardConstants from "../constants/CardConstants";
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
import { readFileSync } from "fs";
import path from "path";
import Inventory from "../database/entities/app/Inventory";
export default class Multidrop extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("multidrop")
.setDescription("Drop 11 cards for the price of 10!");
}
public override async execute(interaction: CommandInteraction) {
if (!CoreClient.AllowDrops) {
await interaction.reply(ErrorMessages.BotSyncing);
return;
}
if (await Config.GetValue("safemode") == "true") {
AppLogger.LogWarn("Commands/Multidrop", ErrorMessages.SafeMode);
await interaction.reply(ErrorMessages.SafeMode);
return;
}
let user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
user = new User(interaction.user.id, CardConstants.StartingCurrency);
await user.Save(User, user);
AppLogger.LogInfo("Commands/Multidrop", `New user (${interaction.user.id}) saved to the database`);
}
if (user.Currency < CardConstants.MultidropCost) {
await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.MultidropCost, user.Currency));
return;
}
user.RemoveCurrency(CardConstants.MultidropCost);
await user.Save(User, user);
const randomCard = CardDropHelperMetadata.GetRandomCard();
const cardsRemaining = CardConstants.MultidropQuantity - 1;
if (!randomCard) {
AppLogger.LogWarn("Commands/Multidrop", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}
await interaction.deferReply();
try {
const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
const imageFileName = randomCard.card.path.split("/").pop()!;
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
const quantityClaimed = inventory ? inventory.Quantity : 0;
const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id);
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
} catch (e) {
AppLogger.LogError("Commands/Multidrop", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
}
}
}

View file

@ -16,11 +16,18 @@ export default class Sacrifice extends Command {
x
.setName("cardnumber")
.setDescription("The card to sacrifice from your inventory")
.setRequired(true));
.setRequired(true))
.addNumberOption(x =>
x
.setName("quantity")
.setDescription("The amount to sacrifice (default 1)"));
}
public override async execute(interaction: CommandInteraction<CacheType>): Promise<void> {
const cardnumber = interaction.options.get("cardnumber", true);
const quantityInput = interaction.options.get("quantity")?.value ?? 1;
const quantity = Number(quantityInput) || 1;
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardnumber.value! as string);
@ -29,6 +36,11 @@ export default class Sacrifice extends Command {
return;
}
if (cardInInventory.Quantity < quantity) {
await interaction.reply(`You can only sacrifice what you own! You have ${cardInInventory.Quantity} of this card`);
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardnumber.value! as string);
if (!cardData) {
@ -36,7 +48,7 @@ export default class Sacrifice extends Command {
return;
}
const cardValue = GetSacrificeAmount(cardData.card.type);
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
const cardRarityString = CardRarityToString(cardData.card.type);
const description = [
@ -44,6 +56,7 @@ export default class Sacrifice extends Command {
`Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`,
];
@ -56,11 +69,11 @@ export default class Sacrifice extends Command {
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!}`)
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!} ${quantity}`)
.setLabel("Confirm")
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!}`)
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!} ${quantity}`)
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary),
]);

View file

@ -26,13 +26,26 @@ export default class Trade extends Command {
x
.setName("receive")
.setDescription("Item to receive")
.setRequired(true));
.setRequired(true))
.addNumberOption(x =>
x
.setName("givequantity")
.setDescription("Amount to give"))
.addNumberOption(x =>
x
.setName("receivequantity")
.setDescription("Amount to receive"));
}
public override async execute(interaction: CommandInteraction) {
const user = interaction.options.get("user", true).user!;
const give = interaction.options.get("give", true);
const receive = interaction.options.get("receive", true);
const givequantityInput = interaction.options.get("givequantity")?.value ?? 1;
const receivequantityInput = interaction.options.get("receivequantity")?.value ?? 1;
const givequantity = Number(givequantityInput) || 1;
const receivequantity = Number(receivequantityInput) || 1;
AppLogger.LogSilly("Commands/Trade", `Parameters: user=${user.id}, give=${give.value}, receive=${receive.value}`);
@ -44,12 +57,12 @@ export default class Trade extends Command {
const user1ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, give.value!.toString());
const user2ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(user.id, receive.value!.toString());
if (!user1ItemEntity) {
if (!user1ItemEntity || user1ItemEntity.Quantity < givequantity) {
await interaction.reply("You do not have the item you are trying to trade.");
return;
}
if (!user2ItemEntity) {
if (!user2ItemEntity || user2ItemEntity.Quantity < receivequantity) {
await interaction.reply("The user you are trying to trade with does not have the item you are trying to trade for.");
return;
}
@ -78,12 +91,12 @@ export default class Trade extends Command {
.addFields([
{
name: `${interaction.user.username} Receives`,
value: `${user2Item.id}: ${user2Item.name}`,
value: `${user2Item.id}: ${user2Item.name} x${receivequantity}`,
inline: true,
},
{
name: `${user.username} Receives`,
value: `${user1Item.id}: ${user1Item.name}`,
value: `${user1Item.id}: ${user1Item.name} x${givequantity}`,
inline: true,
},
{
@ -92,16 +105,16 @@ export default class Trade extends Command {
}
]);
const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, user1Item.id, user2Item.id, user1Item.name, user2Item.name), 1000 * 60 * 15); // 15 minutes
const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, user1Item.id, user2Item.id, user1Item.name, user2Item.name, givequantity, receivequantity), 1000 * 60 * 15); // 15 minutes
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setCustomId(`trade accept ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId}`)
.setCustomId(`trade accept ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId} ${givequantity} ${receivequantity}`)
.setLabel("Accept")
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`trade decline ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId}`)
.setCustomId(`trade decline ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId} ${givequantity} ${receivequantity}`)
.setLabel("Decline")
.setStyle(ButtonStyle.Danger),
]);
@ -109,7 +122,7 @@ export default class Trade extends Command {
await interaction.reply({ content: `${user}`, embeds: [ tradeEmbed ], components: [ row ] });
}
private async autoDecline(interaction: CommandInteraction, user1Username: string, user2Username: string, user1CardNumber: string, user2CardNumber: string, user1CardName: string, user2CardName: string) {
private async autoDecline(interaction: CommandInteraction, user1Username: string, user2Username: string, user1CardNumber: string, user2CardNumber: string, user1CardName: string, user2CardName: string, user1Quantity: number, user2Quantity: number) {
AppLogger.LogSilly("Commands/Trade/AutoDecline", `Auto declining trade between ${user1Username} and ${user2Username}`);
const tradeEmbed = new EmbedBuilder()
@ -120,12 +133,12 @@ export default class Trade extends Command {
.addFields([
{
name: `${user1Username} Receives`,
value: `${user2CardNumber}: ${user2CardName}`,
value: `${user2CardNumber}: ${user2CardName} x${user2Quantity}`,
inline: true,
},
{
name: `${user2Username} Receives`,
value: `${user1CardNumber}: ${user1CardName}`,
value: `${user1CardNumber}: ${user1CardName} x${user1Quantity}`,
inline: true,
},
{

View file

@ -1,11 +1,7 @@
import { AttachmentBuilder, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import { CoreClient } from "../client/client";
import { readFileSync } from "fs";
import path from "path";
import Inventory from "../database/entities/app/Inventory";
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
import AppLogger from "../client/appLogger";
import CardSearchHelper from "../helpers/CardSearchHelper";
export default class View extends Command {
constructor() {
@ -13,70 +9,32 @@ export default class View extends Command {
this.CommandBuilder = new SlashCommandBuilder()
.setName("view")
.setDescription("View a specific command")
.setDescription("Search for a card by its name")
.addStringOption(x =>
x
.setName("cardnumber")
.setDescription("The card number to view")
.setName("name")
.setDescription("The card name to search for")
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const cardNumber = interaction.options.get("cardnumber");
const name = interaction.options.get("name", true);
AppLogger.LogSilly("Commands/View", `Parameters: cardNumber=${cardNumber?.value}`);
if (!cardNumber || !cardNumber.value) {
await interaction.reply("Card number is required.");
return;
}
const card = CoreClient.Cards
.flatMap(x => x.cards)
.find(x => x.id == cardNumber.value);
if (!card) {
await interaction.reply("Card not found.");
return;
}
const series = CoreClient.Cards
.find(x => x.cards.includes(card))!;
let image: Buffer;
const imageFileName = card.path.split("/").pop()!;
try {
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
} catch {
AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
await interaction.reply(`Unable to fetch image for card ${card.id}.`);
return;
}
AppLogger.LogSilly("Commands/View", `Parameters: name=${name.value}`);
await interaction.deferReply();
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const searchResult = await CardSearchHelper.GenerateSearchQuery(name.value!.toString(), interaction.user.id, 7);
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id);
const quantityClaimed = inventory ? inventory.Quantity : 0;
const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
try {
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
});
} catch (e) {
AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${e}`);
if (e instanceof DiscordAPIError) {
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}.`);
} else {
await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN.");
}
if (!searchResult) {
await interaction.editReply("No results found");
return;
}
await interaction.editReply({
embeds: [ searchResult.embed ],
components: [ searchResult.row ],
files: [ searchResult.attachment ],
});
}
}

View file

@ -3,4 +3,11 @@ export default class CardConstants {
public static readonly TimerGiveAmount = 10;
public static readonly DailyCurrency = 100;
public static readonly StartingCurrency = 300;
// Multidrop
public static readonly MultidropCost = this.ClaimCost * 10;
public static readonly MultidropQuantity = 11;
// Effects
public static readonly UnusedChanceUpChance = 1;
}

View file

@ -0,0 +1,19 @@
class EffectDetail {
public readonly id: string;
public readonly friendlyName: string;
public readonly duration: number;
public readonly cost: number;
public readonly cooldown: number;
constructor(id: string, friendlyName: string, duration: number, cost: number, cooldown: number) {
this.id = id;
this.friendlyName = friendlyName;
this.duration = duration;
this.cost = cost;
this.cooldown = cooldown;
}
};
export const EffectDetails = new Map<string, EffectDetail>([
[ "unclaimed", new EffectDetail("unclaimed", "Unclaimed Chance Up", 10 * 60 * 1000, 100, 3 * 60 * 60 * 1000) ],
]);

View file

@ -0,0 +1,8 @@
export default class ErrorMessages {
public static readonly BotSyncing = "Bot is currently syncing, please wait until its done.";
public static readonly SafeMode = "Safe Mode has been activated, please resync to continue.";
public static readonly UnableToFetchCard = "Unable to fetch card, please try again.";
public static readonly UnableToFetchUser = "Unable to fetch user, please try again.";
public static readonly NotEnoughCurrency = (need: number, have: number) => `Not enough currency! You need ${need} currency, you have ${have}!`;
}

View file

@ -0,0 +1,10 @@
import {Environment} from "../constants/Environment";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
interface StringDropdownEventItem {
DropdownId: string,
Event: StringDropdownEvent,
Environment: Environment,
}
export default StringDropdownEventItem;

View file

@ -29,16 +29,16 @@ export default class Inventory extends AppBaseEntity {
this.Quantity = quantity;
}
public AddQuantity(amount: number) {
this.Quantity += amount;
}
public RemoveQuantity(amount: number) {
if (this.Quantity < amount) return;
this.Quantity -= amount;
}
public AddQuantity(amount: number) {
this.Quantity += amount;
}
public AddClaim(claim: Claim) {
this.Claims.push(claim);
}

View file

@ -13,7 +13,7 @@ export default class User extends AppBaseEntity {
@Column()
Currency: number;
@Column()
@Column({ nullable: true })
LastUsedDaily?: Date;
public AddCurrency(amount: number) {

View file

@ -0,0 +1,74 @@
import {Column, Entity} from "typeorm";
import AppBaseEntity from "../../../contracts/AppBaseEntity";
import AppDataSource from "../../dataSources/appDataSource";
@Entity()
export default class UserEffect extends AppBaseEntity {
constructor(name: string, userId: string, unused: number, WhenExpires?: Date) {
super();
this.Name = name;
this.UserId = userId;
this.Unused = unused;
this.WhenExpires = WhenExpires;
}
@Column()
Name: string;
@Column()
UserId: string;
@Column()
Unused: number;
@Column({ nullable: true })
WhenExpires?: Date;
public AddUnused(amount: number) {
this.Unused += amount;
}
public UseEffect(whenExpires: Date): boolean {
if (this.Unused == 0) {
return false;
}
this.Unused -= 1;
this.WhenExpires = whenExpires;
return true;
}
public IsEffectActive(): boolean {
const now = new Date();
if (this.WhenExpires && now < this.WhenExpires) {
return true;
}
return false;
}
public static async FetchOneByUserIdAndName(userId: string, name: string): Promise<UserEffect | null> {
const repository = AppDataSource.getRepository(UserEffect);
const single = await repository.findOne({ where: { UserId: userId, Name: name } });
return single;
}
public static async FetchAllByUserIdPaginated(userId: string, page: number = 0, itemsPerPage: number = 10): Promise<[UserEffect[], number]> {
const repository = AppDataSource.getRepository(UserEffect);
const query = await repository.createQueryBuilder("effect")
.where("effect.UserId = :userId", { userId })
.where("effect.Unused > 0")
.orderBy("effect.Name", "ASC")
.skip(page * itemsPerPage)
.take(itemsPerPage)
.getManyAndCount();
return query;
}
}

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import MigrationHelper from "../../../../helpers/MigrationHelper";
export class CreateUserEffect1729962056556 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up("1729962056556-createUserEffect", "0.9", [
"01-table-userEffect",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Down("1729962056556-createUserEffect", "0.9", [
"01-table-userEffect",
], queryRunner);
}
}

View file

@ -1,11 +1,12 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { CardRarity, CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
import { CardRarity, CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity";
import CardRarityChances from "../constants/CardRarityChances";
import { DropResult } from "../contracts/SeriesMetadata";
import { CoreClient } from "../client/client";
import AppLogger from "../client/appLogger";
import CardConstants from "../constants/CardConstants";
import StringTools from "./StringTools";
import Inventory from "../database/entities/app/Inventory";
export default class CardDropHelperMetadata {
public static GetRandomCard(): DropResult | undefined {
@ -58,6 +59,64 @@ export default class CardDropHelperMetadata {
};
}
public static async GetRandomCardUnclaimed(userId: string): Promise<DropResult | undefined> {
const randomRarity = Math.random() * 100;
let cardRarity: CardRarity;
const bronzeChance = CardRarityChances.Bronze;
const silverChance = bronzeChance + CardRarityChances.Silver;
const goldChance = silverChance + CardRarityChances.Gold;
const mangaChance = goldChance + CardRarityChances.Manga;
if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga;
else cardRarity = CardRarity.Legendary;
const randomCard = await this.GetRandomCardByRarityUnclaimed(cardRarity, userId);
AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardUnclaimed", `Random card: ${randomCard?.card.id} ${randomCard?.card.name}`);
return randomCard;
}
public static async GetRandomCardByRarityUnclaimed(rarity: CardRarity, userId: string): Promise<DropResult | undefined> {
AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Parameters: rarity=${rarity}, userId=${userId}`);
const claimedCards = await Inventory.FetchAllByUserId(userId);
if (!claimedCards) {
// They don't have any cards, so safe to get any random card
return this.GetRandomCardByRarity(rarity);
}
const allCards = CoreClient.Cards
.flatMap(x => x.cards)
.filter(x => x.type == rarity)
.filter(x => !claimedCards.find(y => y.CardNumber == x.id));
const randomCardIndex = Math.floor(Math.random() * allCards.length);
const card = allCards[randomCardIndex];
const series = CoreClient.Cards
.find(x => x.cards.includes(card));
if (!series) {
AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Series not found for card ${card.id}`);
return undefined;
}
AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Random card: ${card.id} ${card.name}`);
return {
series: series,
card: card,
};
}
public static GetCardByCardNumber(cardNumber: string): DropResult | undefined {
AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Parameters: cardNumber=${cardNumber}`);
@ -89,7 +148,7 @@ export default class CardDropHelperMetadata {
const hexCode = Number("0x" + drop.card.colour);
if (hexCode) {
colour = hexCode;
colour = hexCode;
} else {
AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
}
@ -149,4 +208,26 @@ export default class CardDropHelperMetadata {
.setLabel("Reroll")
.setStyle(ButtonStyle.Secondary));
}
public static GenerateMultidropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, cardsRemaining: number, claimedBy?: string, currency?: number): EmbedBuilder {
const dropEmbed = this.GenerateDropEmbed(drop, quantityClaimed, imageFileName, claimedBy, currency);
dropEmbed.setFooter({ text: `${dropEmbed.data.footer?.text} · ${cardsRemaining} Remaining`});
return dropEmbed;
}
public static GenerateMultidropButtons(drop: DropResult, cardsRemaining: number, userId: string, disabled = false): ActionRowBuilder<ButtonBuilder> {
return new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`multidrop keep ${drop.card.id} ${cardsRemaining} ${userId}`)
.setLabel("Keep")
.setStyle(ButtonStyle.Primary)
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`multidrop sacrifice ${drop.card.id} ${cardsRemaining} ${userId}`)
.setLabel(`Sacrifice (+${GetSacrificeAmount(drop.card.type)} 🪙)`)
.setStyle(ButtonStyle.Secondary));
}
}

View file

@ -0,0 +1,117 @@
import {ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
import Fuse from "fuse.js";
import {CoreClient} from "../client/client.js";
import CardDropHelperMetadata from "./CardDropHelperMetadata.js";
import Inventory from "../database/entities/app/Inventory.js";
import {readFileSync} from "fs";
import path from "path";
import AppLogger from "../client/appLogger.js";
interface ReturnedPage {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
attachment: AttachmentBuilder,
results: string[],
}
export default class CardSearchHelper {
public static async GenerateSearchQuery(query: string, userid: string, pages: number): Promise<ReturnedPage | undefined> {
AppLogger.LogSilly("CardSearchHelper/GenerateSearchQuery", `Parameters: query=${query}, userid=${userid}, pages=${pages}`);
const fzf = new Fuse(CoreClient.Cards.flatMap(x => x.cards), { keys: ["name"] });
const entries = fzf.search(query)
.splice(0, pages);
const entry = entries[0];
const results = entries
.flatMap(x => x.item.id);
if (!entry) {
AppLogger.LogVerbose("CardSearchHelper/GenerateSearchQuery", `Unable to find entry: ${query}`);
return undefined;
}
const card = CardDropHelperMetadata.GetCardByCardNumber(entry.item.id);
if (!card) return undefined;
let image: Buffer;
const imageFileName = card.card.path.split("/").pop()!;
try {
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
} catch {
AppLogger.LogError("CardSearchHelper/GenerateSearchQuery", `Unable to fetch image for card ${card.card.id}.`);
return undefined;
}
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id);
const quantityClaimed = inventory?.Quantity ?? 0;
const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`view 0 ${results.join(" ")}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId(`view 2 ${results.join(" ")}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(pages == 1));
return { embed, row, attachment, results };
}
public static async GenerateSearchPageFromQuery(results: string[], userid: string, page: number): Promise<ReturnedPage | undefined> {
const currentPageId = results[page - 1];
const card = CardDropHelperMetadata.GetCardByCardNumber(currentPageId);
if (!card) {
AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to find card by id: ${currentPageId}.`);
return undefined;
}
let image: Buffer;
const imageFileName = card.card.path.split("/").pop()!;
try {
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
} catch {
AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to fetch image for card ${card.card.id}.`);
return undefined;
}
const attachment = new AttachmentBuilder(image, { name: imageFileName });
const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id);
const quantityClaimed = inventory?.Quantity ?? 0;
const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`view ${page - 1} ${results.join(" ")}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(page - 1 == 0),
new ButtonBuilder()
.setCustomId(`view ${page + 1} ${results.join(" ")}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == results.length));
return { embed, row, attachment, results };
}
}

115
src/helpers/EffectHelper.ts Normal file
View file

@ -0,0 +1,115 @@
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
import UserEffect from "../database/entities/app/UserEffect";
import EmbedColours from "../constants/EmbedColours";
import {EffectDetails} from "../constants/EffectDetails";
export default class EffectHelper {
public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) {
let effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
if (!effect) {
effect = new UserEffect(name, userId, quantity);
} else {
effect.AddUnused(quantity);
}
await effect.Save(UserEffect, effect);
}
public static async UseEffect(userId: string, name: string, whenExpires: Date): Promise<boolean> {
const canUseEffect = await this.CanUseEffect(userId, name);
if (!canUseEffect) return false;
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
if (!effect) return false;
effect.UseEffect(whenExpires);
await effect.Save(UserEffect, effect);
return true;
}
public static async CanUseEffect(userId: string, name: string): Promise<boolean> {
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
const now = new Date();
if (!effect || effect.Unused == 0) {
return false;
}
const effectDetail = EffectDetails.get(effect.Id);
if (!effectDetail) {
return false;
}
if (effect.WhenExpires && now < new Date(effect.WhenExpires.getMilliseconds() + effectDetail.cooldown)) {
return false;
}
return true;
}
public static async HasEffect(userId: string, name: string): Promise<boolean> {
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
const now = new Date();
if (!effect || !effect.WhenExpires) {
return false;
}
if (now > effect.WhenExpires) {
return false;
}
return true;
}
public static async GenerateEffectEmbed(userId: string, page: number): Promise<{
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
}> {
const itemsPerPage = 10;
const query = await UserEffect.FetchAllByUserIdPaginated(userId, page - 1, itemsPerPage);
const effects = query[0];
const count = query[1];
const totalPages = count > 0 ? Math.ceil(count / itemsPerPage) : 1;
let description = "*none*";
if (effects.length > 0) {
description = effects.map(x => `${x.Name} x${x.Unused}`).join("\n");
}
const embed = new EmbedBuilder()
.setTitle("Effects")
.setDescription(description)
.setColor(EmbedColours.Ok)
.setFooter({ text: `Page ${page} of ${totalPages}` });
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`effects list ${page - 1}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == 1),
new ButtonBuilder()
.setCustomId(`effects list ${page + 1}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == totalPages),
);
return {
embed,
row,
};
}
}

View file

@ -3,7 +3,7 @@ import path from "path";
import AppLogger from "../client/appLogger";
import {existsSync} from "fs";
import Inventory from "../database/entities/app/Inventory";
import Jimp from "jimp";
import {Jimp} from "jimp";
interface CardInput {
id: string;
@ -46,7 +46,7 @@ export default class ImageHelper {
}
}
const image = await loadImage(await imageData.getBufferAsync("image/png"));
const image = await loadImage(await imageData.getBuffer("image/png"));
const x = i % gridWidth;
const y = Math.floor(i / gridWidth);

View file

@ -1,4 +1,4 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js";
import Inventory from "../database/entities/app/Inventory";
import { CoreClient } from "../client/client";
import EmbedColours from "../constants/EmbedColours";
@ -24,7 +24,8 @@ interface InventoryPageCards {
interface ReturnedInventoryPage {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
row1: ActionRowBuilder<ButtonBuilder>,
row2: ActionRowBuilder<StringSelectMenuBuilder>,
image: AttachmentBuilder,
}
@ -99,7 +100,7 @@ export default class InventoryHelper {
.setColor(EmbedColours.Ok)
.setImage("attachment://page.png");
const row = new ActionRowBuilder<ButtonBuilder>()
const row1 = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`inventory ${userid} ${page - 1}`)
@ -112,9 +113,23 @@ export default class InventoryHelper {
.setStyle(ButtonStyle.Primary)
.setDisabled(page + 1 == pages.length));
let pageNum = 0;
const row2 = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId("inventory")
.setPlaceholder(`${currentPage.name} (${currentPage.seriesSubpage + 1})`)
.addOptions(...pages.map(x =>
new StringSelectMenuOptionBuilder()
.setLabel(`${x.name} (${x.seriesSubpage + 1})`.substring(0, 100))
.setDescription(`Page ${pageNum + 1}`)
.setDefault(currentPage.id == x.id)
.setValue(`${userid} ${pageNum++}`))));
const buffer = await ImageHelper.GenerateCardImageGrid(currentPage.cards.map(x => ({ id: x.id, path: x.path })));
const image = new AttachmentBuilder(buffer, { name: "page.png" });
return { embed, row, image };
return { embed, row1, row2, image };
}
}

View file

@ -118,4 +118,19 @@ export default class TimeLengthInput {
return desNumber;
}
public static ConvertFromMilliseconds(ms: number): TimeLengthInput {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const remainingSeconds = seconds % 60;
const remainingMinutes = minutes % 60;
const remainingHours = hours % 24;
const timeString = `${days}d ${remainingHours}h ${remainingMinutes}m ${remainingSeconds}s`;
return new TimeLengthInput(timeString);
}
}

View file

@ -7,9 +7,12 @@ import AllBalance from "./commands/allbalance";
import Balance from "./commands/balance";
import Daily from "./commands/daily";
import Drop from "./commands/drop";
import Effects from "./commands/effects";
import Gdrivesync from "./commands/gdrivesync";
import Give from "./commands/give";
import Id from "./commands/id";
import Inventory from "./commands/inventory";
import Multidrop from "./commands/multidrop";
import Resync from "./commands/resync";
import Sacrifice from "./commands/sacrifice";
import Series from "./commands/series";
@ -23,11 +26,17 @@ import Droprarity from "./commands/stage/droprarity";
// Button Event Imports
import Claim from "./buttonEvents/Claim";
import EffectsButtonEvent from "./buttonEvents/Effects";
import InventoryButtonEvent from "./buttonEvents/Inventory";
import MultidropButtonEvent from "./buttonEvents/Multidrop";
import Reroll from "./buttonEvents/Reroll";
import SacrificeButtonEvent from "./buttonEvents/Sacrifice";
import SeriesEvent from "./buttonEvents/Series";
import TradeButtonEvent from "./buttonEvents/Trade";
import ViewButtonEvent from "./buttonEvents/View";
// String Dropdown Event Imports
import InventoryStringDropdown from "./stringDropdowns/Inventory";
export default class Registry {
public static RegisterCommands() {
@ -37,9 +46,12 @@ export default class Registry {
CoreClient.RegisterCommand("balance", new Balance());
CoreClient.RegisterCommand("daily", new Daily());
CoreClient.RegisterCommand("drop", new Drop());
CoreClient.RegisterCommand("effects", new Effects());
CoreClient.RegisterCommand("gdrivesync", new Gdrivesync());
CoreClient.RegisterCommand("give", new Give());
CoreClient.RegisterCommand("id", new Id());
CoreClient.RegisterCommand("inventory", new Inventory());
CoreClient.RegisterCommand("multidrop", new Multidrop());
CoreClient.RegisterCommand("resync", new Resync());
CoreClient.RegisterCommand("sacrifice", new Sacrifice());
CoreClient.RegisterCommand("series", new Series());
@ -54,10 +66,17 @@ export default class Registry {
public static RegisterButtonEvents() {
CoreClient.RegisterButtonEvent("claim", new Claim());
CoreClient.RegisterButtonEvent("effects", new EffectsButtonEvent());
CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent());
CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent());
CoreClient.RegisterButtonEvent("reroll", new Reroll());
CoreClient.RegisterButtonEvent("sacrifice", new SacrificeButtonEvent());
CoreClient.RegisterButtonEvent("series", new SeriesEvent());
CoreClient.RegisterButtonEvent("trade", new TradeButtonEvent());
CoreClient.RegisterButtonEvent("view", new ViewButtonEvent());
}
public static RegisterStringDropdownEvents() {
CoreClient.RegisterStringDropdownEvent("inventory", new InventoryStringDropdown());
}
}

View file

@ -0,0 +1,43 @@
import {StringSelectMenuInteraction} from "discord.js";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
import AppLogger from "../client/appLogger";
import InventoryHelper from "../helpers/InventoryHelper";
export default class Inventory extends StringDropdownEvent {
public override async execute(interaction: StringSelectMenuInteraction) {
if (!interaction.guild) return;
const userid = interaction.values[0].split(" ")[0];
const page = interaction.values[0].split(" ")[1];
AppLogger.LogDebug("StringDropdown/Inventory", `Parameters: userid=${userid}, page=${page}`);
await interaction.deferUpdate();
const member = interaction.guild.members.cache.find(x => x.id == userid) || await interaction.guild.members.fetch(userid);
if (!member) {
await interaction.reply("Unable to find user.");
return;
}
try {
const embed = await InventoryHelper.GenerateInventoryPage(member.user.username, member.user.id, Number(page));
if (!embed) {
await interaction.followUp("No page for user found.");
return;
}
await interaction.editReply({
files: [ embed.image ],
embeds: [ embed.embed ],
components: [ embed.row1, embed.row2 ],
});
} catch (e) {
AppLogger.LogError("StringDropdown/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`);
await interaction.followUp("An error has occurred running this command.");
}
}
}

View file

@ -0,0 +1,5 @@
import {StringSelectMenuInteraction} from "discord.js";
export abstract class StringDropdownEvent {
abstract execute(interaction: StringSelectMenuInteraction): Promise<void>;
}

View file

@ -0,0 +1,127 @@
import {ButtonInteraction} from "discord.js";
import Effects from "../../src/buttonEvents/Effects";
import EffectHelper from "../../src/helpers/EffectHelper";
describe("execute", () => {
describe("GIVEN action in custom id is list", () => {
const interaction = {
customId: "effects list",
} as unknown as ButtonInteraction;
let listSpy: jest.SpyInstance;
beforeAll(async () => {
const effects = new Effects();
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
await effects.execute(interaction);
});
test("EXPECT list function to be called", () => {
expect(listSpy).toHaveBeenCalledTimes(1);
expect(listSpy).toHaveBeenCalledWith(interaction);
});
});
});
describe("List", () => {
let interaction: ButtonInteraction;
const embed = {
name: "Embed",
};
const row = {
name: "Row",
};
beforeEach(() => {
interaction = {
customId: "effects list",
user: {
id: "userId",
},
update: jest.fn(),
reply: jest.fn(),
} as unknown as ButtonInteraction;
});
describe("GIVEN page is a valid number", () => {
beforeEach(async () => {
interaction.customId += " 1";
EffectHelper.GenerateEffectEmbed = jest.fn()
.mockResolvedValue({
embed,
row,
});
const effects = new Effects();
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to be called", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
});
test("EXPECT interaction to be updated", () => {
expect(interaction.update).toHaveBeenCalledTimes(1);
expect(interaction.update).toHaveBeenCalledWith({
embeds: [ embed ],
components: [ row ],
});
});
});
describe("GIVEN page in custom id is not supplied", () => {
beforeEach(async () => {
EffectHelper.GenerateEffectEmbed = jest.fn()
.mockResolvedValue({
embed,
row,
});
const effects = new Effects();
await effects.execute(interaction);
});
test("EXPECT interaction to be replied with error", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number");
});
test("EXPECT interaction to not be updated", () => {
expect(interaction.update).not.toHaveBeenCalled();
});
});
describe("GIVEN page in custom id is not a number", () => {
beforeEach(async () => {
interaction.customId += " test";
EffectHelper.GenerateEffectEmbed = jest.fn()
.mockResolvedValue({
embed,
row,
});
const effects = new Effects();
await effects.execute(interaction);
});
test("EXPECT interaction to be replied with error", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number");
});
test("EXPECT interaction to not be updated", () => {
expect(interaction.update).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`constructor EXPECT CommandBuilder to be defined 1`] = `
{
"contexts": undefined,
"default_member_permissions": undefined,
"default_permission": undefined,
"description": "Effects",
"description_localizations": undefined,
"dm_permission": undefined,
"integration_types": undefined,
"name": "effects",
"name_localizations": undefined,
"nsfw": undefined,
"options": [
{
"description": "List all effects I have",
"description_localizations": undefined,
"name": "list",
"name_localizations": undefined,
"options": [
{
"autocomplete": undefined,
"choices": undefined,
"description": "The page number",
"description_localizations": undefined,
"max_value": undefined,
"min_value": 1,
"name": "page",
"name_localizations": undefined,
"required": false,
"type": 10,
},
],
"type": 1,
},
],
"type": 1,
}
`;

View file

@ -0,0 +1,164 @@
import {ChatInputCommandInteraction} from "discord.js";
import Effects from "../../src/commands/effects";
import EffectHelper from "../../src/helpers/EffectHelper";
describe("constructor", () => {
let effects: Effects;
beforeEach(() => {
effects = new Effects();
});
test("EXPECT CommandBuilder to be defined", () => {
expect(effects.CommandBuilder).toMatchSnapshot();
});
});
describe("execute", () => {
describe("GIVEN interaction is not a chat input command", () => {
let interaction: ChatInputCommandInteraction;
let listSpy: jest.SpyInstance;
beforeEach(async () => {
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(false),
} as unknown as ChatInputCommandInteraction;
const effects = new Effects();
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
await effects.execute(interaction);
});
test("EXPECT isChatInputCommand to have been called", () => {
expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1);
});
test("EXPECT nothing to happen", () => {
expect(listSpy).not.toHaveBeenCalled();
});
});
describe("GIVEN subcommand is list", () => {
let interaction: ChatInputCommandInteraction;
let listSpy: jest.SpyInstance;
beforeEach(async () => {
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(true),
options: {
getSubcommand: jest.fn().mockReturnValue("list"),
},
} as unknown as ChatInputCommandInteraction;
const effects = new Effects();
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
await effects.execute(interaction);
});
test("EXPECT subcommand function to be called", () => {
expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1);
});
test("EXPECT list function to be called", () => {
expect(listSpy).toHaveBeenCalledTimes(1);
expect(listSpy).toHaveBeenCalledWith(interaction);
});
});
});
describe("List", () => {
const effects: Effects = new Effects();
let interaction: ChatInputCommandInteraction;
const embed = {
name: "embed",
};
const row = {
name: "row",
};
beforeEach(async () => {
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(true),
options: {
getSubcommand: jest.fn().mockReturnValue("list"),
},
reply: jest.fn(),
user: {
id: "userId",
},
} as unknown as ChatInputCommandInteraction;
const effects = new Effects();
EffectHelper.GenerateEffectEmbed = jest.fn().mockReturnValue({
embed,
row,
});
jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
});
describe("GIVEN page option is supplied", () => {
describe("AND page is a valid number", () => {
beforeEach(async () => {
interaction.options.get = jest.fn().mockReturnValueOnce({
value: "2",
});
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 2);
});
test("EXPECT interaction to have been replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith({
embeds: [ embed ],
components: [ row ],
});
});
});
describe("AND page is not a valid number", () => {
beforeEach(async () => {
interaction.options.get = jest.fn().mockReturnValueOnce({
value: "test",
});
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page of 1", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
});
});
});
describe("GIVEN page option is not supplied", () => {
beforeEach(async () => {
interaction.options.get = jest.fn().mockReturnValueOnce(undefined);
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page of 1", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
});
});
});

View file

@ -0,0 +1,103 @@
import UserEffect from "../../../../src/database/entities/app/UserEffect";
let userEffect: UserEffect;
const now = new Date();
beforeEach(() => {
userEffect = new UserEffect("name", "userId", 1);
});
describe("AddUnused", () => {
beforeEach(() => {
userEffect.AddUnused(1);
});
test("EXPECT unused to be the amount more", () => {
expect(userEffect.Unused).toBe(2);
});
});
describe("UseEffect", () => {
describe("GIVEN Unused is 0", () => {
let result: boolean;
beforeEach(() => {
userEffect.Unused = 0;
result = userEffect.UseEffect(now);
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
test("EXPECT details not to be changed", () => {
expect(userEffect.Unused).toBe(0);
expect(userEffect.WhenExpires).toBeUndefined();
});
});
describe("GIVEN Unused is greater than 0", () => {
let result: boolean;
beforeEach(() => {
result = userEffect.UseEffect(now);
});
test("EXPECT true returned", () => {
expect(result).toBe(true);
});
test("EXPECT Unused to be subtracted by 1", () => {
expect(userEffect.Unused).toBe(0);
});
test("EXPECT WhenExpires to be set", () => {
expect(userEffect.WhenExpires).toBe(now);
});
});
});
describe("IsEffectActive", () => {
describe("GIVEN WhenExpires is null", () => {
let result: boolean;
beforeEach(() => {
result = userEffect.IsEffectActive();
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
describe("GIVEN WhenExpires is defined", () => {
describe("AND WhenExpires is in the past", () => {
let result: boolean;
beforeEach(() => {
userEffect.WhenExpires = new Date(now.getTime() - 100);
result = userEffect.IsEffectActive();
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
describe("AND WhenExpires is in the future", () => {
let result: boolean;
beforeEach(() => {
userEffect.WhenExpires = new Date(now.getTime() + 100);
result = userEffect.IsEffectActive();
});
test("EXPECT true returned", () => {
expect(result).toBe(true);
});
});
});
});

View file

@ -0,0 +1,380 @@
import {ActionRowBuilder, ButtonBuilder, EmbedBuilder} from "discord.js";
import UserEffect from "../../src/database/entities/app/UserEffect";
import EffectHelper from "../../src/helpers/EffectHelper";
describe("AddEffectToUserInventory", () => {
describe("GIVEN effect is in database", () => {
const effectMock = {
AddUnused: jest.fn(),
Save: jest.fn(),
};
beforeAll(async () => {
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(effectMock);
await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
});
test("EXPECT database to be fetched", () => {
expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledTimes(1);
expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledWith("userId", "name");
});
test("EXPECT effect to be updated", () => {
expect(effectMock.AddUnused).toHaveBeenCalledTimes(1);
expect(effectMock.AddUnused).toHaveBeenCalledWith(1);
});
test("EXPECT effect to be saved", () => {
expect(effectMock.Save).toHaveBeenCalledTimes(1);
expect(effectMock.Save).toHaveBeenCalledWith(UserEffect, effectMock);
});
});
describe("GIVEN effect is not in database", () => {
beforeAll(async () => {
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
UserEffect.prototype.Save = jest.fn();
await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
});
test("EXPECT effect to be saved", () => {
expect(UserEffect.prototype.Save).toHaveBeenCalledTimes(1);
expect(UserEffect.prototype.Save).toHaveBeenCalledWith(UserEffect, expect.any(UserEffect));
});
});
});
describe("UseEffect", () => {
describe("GIVEN effect is in database", () => {
describe("GIVEN now is before effect.WhenExpires", () => {
let result: boolean | undefined;
// nowMock < whenExpires
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpires = new Date(2024, 11, 3, 14, 0);
const userEffect = {
Unused: 1,
WhenExpires: whenExpires,
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.UseEffect("userId", "name", new Date());
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
describe("GIVEN currently used effect is inactive", () => {
let result: boolean | undefined;
// nowMock > whenExpires
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpires = new Date(2024, 11, 3, 13, 0);
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
const userEffect = {
Unused: 1,
WhenExpires: whenExpires,
UseEffect: jest.fn(),
Save: jest.fn(),
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
});
test("EXPECT UseEffect to be called", () => {
expect(userEffect.UseEffect).toHaveReturnedTimes(1);
expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew);
});
test("EXPECT effect to be saved", () => {
expect(userEffect.Save).toHaveBeenCalledTimes(1);
expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect);
});
test("EXPECT true returned", () => {
expect(result).toBe(true);
});
});
describe("GIVEN effect.WhenExpires is null", () => {
let result: boolean | undefined;
// nowMock > whenExpires
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
const userEffect = {
Unused: 1,
WhenExpires: null,
UseEffect: jest.fn(),
Save: jest.fn(),
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
});
test("EXPECT UseEffect to be called", () => {
expect(userEffect.UseEffect).toHaveBeenCalledTimes(1);
expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew);
});
test("EXPECT effect to be saved", () => {
expect(userEffect.Save).toHaveBeenCalledTimes(1);
expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect);
});
test("EXPECT true returned", () => {
expect(result).toBe(true);
});
});
});
describe("GIVEN effect is not in database", () => {
let result: boolean | undefined;
// nowMock > whenExpires
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
describe("GIVEN effect.Unused is 0", () => {
let result: boolean | undefined;
// nowMock > whenExpires
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
const userEffect = {
Unused: 0,
WhenExpires: null,
UseEffect: jest.fn(),
Save: jest.fn(),
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
});
describe("HasEffect", () => {
describe("GIVEN effect is in database", () => {
describe("GIVEN effect.WhenExpires is defined", () => {
describe("GIVEN now is before effect.WhenExpires", () => {
let result: boolean | undefined;
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpires = new Date(2024, 11, 3, 15, 0);
const userEffect = {
WhenExpires: whenExpires,
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.HasEffect("userId", "name");
});
test("EXPECT true returned", () => {
expect(result).toBe(true);
});
});
describe("GIVEN now is after effect.WhenExpires", () => {
let result: boolean | undefined;
const nowMock = new Date(2024, 11, 3, 16, 30);
const whenExpires = new Date(2024, 11, 3, 15, 0);
const userEffect = {
WhenExpires: whenExpires,
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.HasEffect("userId", "name");
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
});
describe("GIVEN effect.WhenExpires is undefined", () => {
let result: boolean | undefined;
const userEffect = {
WhenExpires: undefined,
};
beforeAll(async () => {
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
result = await EffectHelper.HasEffect("userId", "name");
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
});
describe("GIVEN effect is not in database", () => {
let result: boolean | undefined;
beforeAll(async () => {
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
result = await EffectHelper.HasEffect("userId", "name");
});
test("EXPECT false returned", () => {
expect(result).toBe(false);
});
});
});
describe("GenerateEffectEmbed", () => {
beforeEach(async () => {
UserEffect.FetchAllByUserIdPaginated = jest.fn()
.mockResolvedValue([
[],
0,
]);
await EffectHelper.GenerateEffectEmbed("userId", 1);
});
test("EXPECT UserEffect.FetchAllByUserIdPaginated to be called", () => {
expect(UserEffect.FetchAllByUserIdPaginated).toHaveBeenCalledTimes(1);
expect(UserEffect.FetchAllByUserIdPaginated).toHaveBeenCalledWith("userId", 0, 10);
});
describe("GIVEN there are no effects returned", () => {
let result: {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
};
beforeEach(async () => {
UserEffect.FetchAllByUserIdPaginated = jest.fn()
.mockResolvedValue([
[],
0,
]);
result = await EffectHelper.GenerateEffectEmbed("userId", 1);
});
test("EXPECT result returned", () => {
expect(result).toMatchSnapshot();
});
});
describe("GIVEN there are effects returned", () => {
let result: {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
};
beforeEach(async () => {
UserEffect.FetchAllByUserIdPaginated = jest.fn()
.mockResolvedValue([
[
{
Name: "name",
Unused: 1,
},
],
1,
]);
result = await EffectHelper.GenerateEffectEmbed("userId", 1);
});
test("EXPECT result returned", () => {
expect(result).toMatchSnapshot();
});
describe("AND it is the first page", () => {
beforeEach(async () => {
result = await EffectHelper.GenerateEffectEmbed("userId", 1)
});
test("EXPECT Previous button to be disabled", () => {
const button = result.row.components[0].data as unknown as {
label: string,
disabled: boolean
};
expect(button).toBeDefined();
expect(button.label).toBe("Previous");
expect(button.disabled).toBe(true);
});
});
describe("AND it is the last page", () => {
beforeEach(async () => {
result = await EffectHelper.GenerateEffectEmbed("userId", 1)
});
test("EXPECT Next button to be disabled", () => {
const button = result.row.components[1].data as unknown as {
label: string,
disabled: boolean
};
expect(button).toBeDefined();
expect(button.label).toBe("Next");
expect(button.disabled).toBe(true);
});
});
});
});

View file

@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GenerateEffectEmbed GIVEN there are effects returned EXPECT result returned 1`] = `
{
"embed": {
"color": 3166394,
"description": "name x1",
"footer": {
"icon_url": undefined,
"text": "Page 1 of 1",
},
"title": "Effects",
},
"row": {
"components": [
{
"custom_id": "effects list 0",
"disabled": true,
"emoji": undefined,
"label": "Previous",
"style": 1,
"type": 2,
},
{
"custom_id": "effects list 2",
"disabled": true,
"emoji": undefined,
"label": "Next",
"style": 1,
"type": 2,
},
],
"type": 1,
},
}
`;
exports[`GenerateEffectEmbed GIVEN there are no effects returned EXPECT result returned 1`] = `
{
"embed": {
"color": 3166394,
"description": "*none*",
"footer": {
"icon_url": undefined,
"text": "Page 1 of 1",
},
"title": "Effects",
},
"row": {
"components": [
{
"custom_id": "effects list 0",
"disabled": true,
"emoji": undefined,
"label": "Previous",
"style": 1,
"type": 2,
},
{
"custom_id": "effects list 2",
"disabled": true,
"emoji": undefined,
"label": "Next",
"style": 1,
"type": 2,
},
],
"type": 1,
},
}
`;

View file

@ -1,66 +0,0 @@
import {CoreClient} from "../src/client/client";
import Registry from "../src/registry";
import fs from "fs";
import path from "path";
describe("RegisterCommands", () => {
test("EXPECT every command in the commands folder to be registered", () => {
const registeredCommands: string[] = [];
CoreClient.RegisterCommand = jest.fn().mockImplementation((name: string) => {
registeredCommands.push(name);
});
Registry.RegisterCommands();
const commandFiles = getFilesInDirectory(path.join(process.cwd(), "src", "commands"))
.filter(x => x.endsWith(".ts"));
for (const file of commandFiles) {
expect(registeredCommands).toContain(file.split("/").pop()!.split(".")[0]);
}
expect(commandFiles.length).toBe(registeredCommands.length);
});
});
describe("RegisterButtonEvents", () => {
test("EXEPCT every button event in the button events folder to be registered", () => {
const registeredButtonEvents: string[] = [];
CoreClient.RegisterButtonEvent = jest.fn().mockImplementation((name: string) => {
registeredButtonEvents.push(name);
});
Registry.RegisterButtonEvents();
const eventFiles = getFilesInDirectory(path.join(process.cwd(), "src", "buttonEvents"))
.filter(x => x.endsWith(".ts"));
for (const file of eventFiles) {
expect(registeredButtonEvents).toContain(file.split("/").pop()!.split(".")[0].toLowerCase());
}
expect(eventFiles.length).toBe(registeredButtonEvents.length);
});
});
function getFilesInDirectory(dir: string): string[] {
let results: string[] = [];
const list = fs.readdirSync(dir);
list.forEach(file => {
file = path.join(dir, file);
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* recurse into a subdirectory */
results = results.concat(getFilesInDirectory(file));
} else {
/* is a file */
results.push(file);
}
});
return results;
}

3011
yarn.lock

File diff suppressed because it is too large Load diff