mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat: @discordjs/structures (#10900)
* chore: init /structures * feat: base structure * feat: initial structures design attempt * refactor(Structure): use unknown to store in kData * feat(Structure): add Invite refactor(Structure): patch to _patch * refactor: symbol names and override location * fix: don't possibly return 0 if discord borks Co-authored-by: Synbulat Biishev <signin@syjalo.dev> * refactor: use getter value instead of api Co-authored-by: Synbulat Biishev <signin@syjalo.dev> * refactor: cache createdTimestamp value Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> * docs: better docs for what's done so far * feat: add Mixin * refactor(User): remove bitfield getters and add displayName * feat(structures): add Connection * feat(structures): add Channel base * refactor(Mixin): trace prototype chain, allow construction * fix(structures): fix mixin behavior * fix(structures): data optimization call behavior from perf testing * feat: channel mixins * chore: update deps * feat: channels and mixins * chore: more typeguard tests * fix: tests and some other issues * feat: add ChannelWebhookMixin * fix: more tests * chore: tests and docs * chore: docs * fix: remove unneccessary omitted * chore: apply code suggestions * refactor: change how extended invite works * fix: type imports * Apply suggestions from code review Co-authored-by: Almeida <github@almeidx.dev> * fix: tests * chore: add jsdoc * refactor: apply code suggestions * fix: don't instantiate sub-structures * fix: don't do null default twice * chore: use formatters, add _cache * chore: lockfile * chore: move MixinTypes to declaratiion file * fix: tests * fix: don't include source d.ts files for docs * feat: bitfields * feat: more bitfields * refactor: remove DirectoryChannel structure * chore: apply suggestions from code review * chore: remove unused import * refactor: use symbol for mixin toJSON, remove _ prefix * chore: apply suggestions from code review * refactor: remove bitfield casts * refactor: remove special case for threadchannel types * fix: apply code review suggestions * refactor: bitfields always store bigint * fix: tests * chore: apply suggestions from code review * fix: lint * refactor: conditional structuredClone * Apply suggestions from code review Co-authored-by: ckohen <chaikohen@gmail.com> * fix: code review errors * fix: lint * chore: bump dtypes * Update packages/structures/cliff.toml Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * docs: link to VideoQualityMode * chore: typo in comment * chore: small nits in docs links * chore: small nits * docs: forgot one * chore: update template * chore: typos and things * chore: apply suggestions from code review * fix: tests and typeguards * chore: don't clone appliedTags * refactor: use a symbol for patch method * fix: add missing readonly * chore: remove todo comment * refactor: use symbol for clone * fix: add constraint to DataType * chore: apply suggestions * fix: dtypes bump * chore: fix comment * chore: add todo comment * chore: mark bitfield as todo chore: mark bit field as todo and edit readme --------- Co-authored-by: ckohen <chaikohen@gmail.com> Co-authored-by: Synbulat Biishev <signin@syjalo.dev> Co-authored-by: Almeida <github@almeidx.dev> Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -27,6 +27,7 @@ pnpm-lock.yaml @discordjs/core
|
|||||||
/packages/proxy-container/ @discordjs/proxy
|
/packages/proxy-container/ @discordjs/proxy
|
||||||
/packages/rest/ @discordjs/rest
|
/packages/rest/ @discordjs/rest
|
||||||
/packages/scripts/ @discordjs/scripts
|
/packages/scripts/ @discordjs/scripts
|
||||||
|
/packages/structures/ @discordjs/structures
|
||||||
/packages/ui/ @discordjs/ui
|
/packages/ui/ @discordjs/ui
|
||||||
/packages/util/ @discordjs/util
|
/packages/util/ @discordjs/util
|
||||||
/packages/voice/ @discordjs/core
|
/packages/voice/ @discordjs/core
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ body:
|
|||||||
- proxy
|
- proxy
|
||||||
- proxy-container
|
- proxy-container
|
||||||
- rest
|
- rest
|
||||||
|
- structures
|
||||||
- ui
|
- ui
|
||||||
- util
|
- util
|
||||||
- voice
|
- voice
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ body:
|
|||||||
- proxy
|
- proxy
|
||||||
- proxy-container
|
- proxy-container
|
||||||
- rest
|
- rest
|
||||||
|
- structures
|
||||||
- ui
|
- ui
|
||||||
- util
|
- util
|
||||||
- voice
|
- voice
|
||||||
|
|||||||
3
.github/issue-labeler.yml
vendored
3
.github/issue-labeler.yml
vendored
@@ -37,6 +37,9 @@ packages:proxy-container:
|
|||||||
packages:rest:
|
packages:rest:
|
||||||
- "### Which (application|package|application or package) is this (bug
|
- "### Which (application|package|application or package) is this (bug
|
||||||
report|feature request) for\\?\\n\\nrest\\n"
|
report|feature request) for\\?\\n\\nrest\\n"
|
||||||
|
packages:structures:
|
||||||
|
- "### Which (application|package|application or package) is this (bug
|
||||||
|
report|feature request) for\\?\\n\\nstructures\\n"
|
||||||
packages:ui:
|
packages:ui:
|
||||||
- "### Which (application|package|application or package) is this (bug
|
- "### Which (application|package|application or package) is this (bug
|
||||||
report|feature request) for\\?\\n\\ui\\n"
|
report|feature request) for\\?\\n\\ui\\n"
|
||||||
|
|||||||
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -78,6 +78,11 @@ packages:rest:
|
|||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- packages/rest/*
|
- packages/rest/*
|
||||||
- packages/rest/**/*
|
- packages/rest/**/*
|
||||||
|
packages:structures:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- packages/structures/*
|
||||||
|
- packages/structures/**/*
|
||||||
packages:ui:
|
packages:ui:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|||||||
2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@@ -80,6 +80,8 @@
|
|||||||
color: fbca04
|
color: fbca04
|
||||||
- name: packages:rest
|
- name: packages:rest
|
||||||
color: fbca04
|
color: fbca04
|
||||||
|
- name: packages:structures
|
||||||
|
color: fbca04
|
||||||
- name: packages:ui
|
- name: packages:ui
|
||||||
color: fbca04
|
color: fbca04
|
||||||
- name: packages:util
|
- name: packages:util
|
||||||
|
|||||||
1
.github/workflows/deprecate-version.yml
vendored
1
.github/workflows/deprecate-version.yml
vendored
@@ -17,6 +17,7 @@ on:
|
|||||||
- '@discordjs/next'
|
- '@discordjs/next'
|
||||||
- '@discordjs/proxy'
|
- '@discordjs/proxy'
|
||||||
- '@discordjs/rest'
|
- '@discordjs/rest'
|
||||||
|
- '@discordjs/structures'
|
||||||
- '@discordjs/util'
|
- '@discordjs/util'
|
||||||
- '@discordjs/voice'
|
- '@discordjs/voice'
|
||||||
- '@discordjs/ws'
|
- '@discordjs/ws'
|
||||||
|
|||||||
4
.github/workflows/documentation.yml
vendored
4
.github/workflows/documentation.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
|||||||
- name: Build docs with main api-extractor
|
- name: Build docs with main api-extractor
|
||||||
if: ${{ inputs.ref && inputs.ref != 'main' }}
|
if: ${{ inputs.ref && inputs.ref != 'main' }}
|
||||||
run: |
|
run: |
|
||||||
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "util" "voice" "ws")
|
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "structures" "util" "voice" "ws")
|
||||||
for PACKAGE in "${PACKAGES[@]}"; do
|
for PACKAGE in "${PACKAGES[@]}"; do
|
||||||
cd "packages/${PACKAGE}"
|
cd "packages/${PACKAGE}"
|
||||||
sed -i 's!https://github.com/discordjs/discord.js/tree/main!https://github.com/discordjs/discord.js/tree/${{ inputs.ref }}!' api-extractor.json
|
sed -i 's!https://github.com/discordjs/discord.js/tree/main!https://github.com/discordjs/discord.js/tree/${{ inputs.ref }}!' api-extractor.json
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
- name: Move docs to correct directory
|
- name: Move docs to correct directory
|
||||||
if: ${{ env.REF_TYPE == 'branch' }}
|
if: ${{ env.REF_TYPE == 'branch' }}
|
||||||
run: |
|
run: |
|
||||||
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "util" "voice" "ws")
|
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "structures" "util" "voice" "ws")
|
||||||
for PACKAGE in "${PACKAGES[@]}"; do
|
for PACKAGE in "${PACKAGES[@]}"; do
|
||||||
if [[ "${PACKAGE}" == "discord.js" ]]; then
|
if [[ "${PACKAGE}" == "discord.js" ]]; then
|
||||||
mkdir -p "out/${PACKAGE}"
|
mkdir -p "out/${PACKAGE}"
|
||||||
|
|||||||
2
.github/workflows/publish-dev.yml
vendored
2
.github/workflows/publish-dev.yml
vendored
@@ -28,6 +28,8 @@ jobs:
|
|||||||
folder: 'proxy'
|
folder: 'proxy'
|
||||||
- package: '@discordjs/rest'
|
- package: '@discordjs/rest'
|
||||||
folder: 'rest'
|
folder: 'rest'
|
||||||
|
- package: '@discordjs/structures'
|
||||||
|
folder: 'structures'
|
||||||
- package: '@discordjs/util'
|
- package: '@discordjs/util'
|
||||||
folder: 'util'
|
folder: 'util'
|
||||||
- package: '@discordjs/voice'
|
- package: '@discordjs/voice'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"description": "Imagine a bot... the most popular way to build discord bots",
|
"description": "Imagine a bot... the most popular way to build discord bots",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:copy_readme": "cpy \"../../packages/(discord.js|brokers|builders|collection|core|formatters|next|proxy|rest|util|voice|ws)/README.md\" \"src/assets/readme\" --rename='home-{{basename}}'",
|
"build:copy_readme": "cpy \"../../packages/(discord.js|brokers|builders|collection|core|formatters|next|proxy|rest|structures|util|voice|ws)/README.md\" \"src/assets/readme\" --rename='home-{{basename}}'",
|
||||||
"build:check": "tsc --noEmit",
|
"build:check": "tsc --noEmit",
|
||||||
"build:local": "cross-env NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod",
|
"build:local": "cross-env NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod",
|
||||||
"build:prod": "pnpm run build:copy_readme && pnpm run build:next",
|
"build:prod": "pnpm run build:copy_readme && pnpm run build:next",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const PACKAGES = [
|
|||||||
{ name: 'next' },
|
{ name: 'next' },
|
||||||
{ name: 'proxy' },
|
{ name: 'proxy' },
|
||||||
{ name: 'rest' },
|
{ name: 'rest' },
|
||||||
|
{ name: 'structures' },
|
||||||
{ name: 'util' },
|
{ name: 'util' },
|
||||||
{ name: 'voice' },
|
{ name: 'voice' },
|
||||||
{ name: 'ws' },
|
{ name: 'ws' },
|
||||||
|
|||||||
@@ -244,6 +244,14 @@ export default tseslint.config(
|
|||||||
'unicorn/prefer-node-protocol': 0,
|
'unicorn/prefer-node-protocol': 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: [`packages/structures/**/*${commonFiles}`],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-empty-interface': 0,
|
||||||
|
'@typescript-eslint/no-empty-object-type': 0,
|
||||||
|
'@typescript-eslint/no-unsafe-declaration-merging': 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: [`packages/voice/**/*${commonFiles}`],
|
files: [`packages/voice/**/*${commonFiles}`],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ runs:
|
|||||||
flags: rest
|
flags: rest
|
||||||
token: ${{ inputs.CODECOV_TOKEN }}
|
token: ${{ inputs.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload Structures Coverage
|
||||||
|
if: ${{ hashFiles('packages/structures/coverage/cobertura-coverage.xml') != '' }}
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
files: ./packages/structures/coverage/cobertura-coverage.xml
|
||||||
|
disable_search: true
|
||||||
|
flags: structures
|
||||||
|
token: ${{ inputs.CODECOV_TOKEN }}
|
||||||
|
|
||||||
- name: Upload Util Coverage
|
- name: Upload Util Coverage
|
||||||
if: ${{ hashFiles('packages/util/coverage/cobertura-coverage.xml') != '' }}
|
if: ${{ hashFiles('packages/util/coverage/cobertura-coverage.xml') != '' }}
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const PACKAGES = [
|
|||||||
'next',
|
'next',
|
||||||
'proxy',
|
'proxy',
|
||||||
'rest',
|
'rest',
|
||||||
|
'structures',
|
||||||
'util',
|
'util',
|
||||||
'voice',
|
'voice',
|
||||||
'ws',
|
'ws',
|
||||||
|
|||||||
6
packages/structures/.cliff-jumperrc.json
Normal file
6
packages/structures/.cliff-jumperrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "structures",
|
||||||
|
"org": "discordjs",
|
||||||
|
"packagePath": "packages/structures",
|
||||||
|
"identifierBase": false
|
||||||
|
}
|
||||||
28
packages/structures/.gitignore
vendored
Normal file
28
packages/structures/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Packages
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Dist
|
||||||
|
dist
|
||||||
|
dist-docs
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
docs/**/*
|
||||||
|
!docs/README.md
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
.turbo
|
||||||
|
.tmp
|
||||||
|
coverage
|
||||||
2
packages/structures/.lintstagedrc.js
Normal file
2
packages/structures/.lintstagedrc.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/** @type {import('lint-staged').Config} */
|
||||||
|
module.exports = require('../../.lintstagedrc.json');
|
||||||
7
packages/structures/.prettierignore
Normal file
7
packages/structures/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.turbo
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
dist-docs
|
||||||
|
docs/docs.api.json
|
||||||
|
CHANGELOG.md
|
||||||
|
tsup.config.bundled*
|
||||||
2
packages/structures/.prettierrc.js
Normal file
2
packages/structures/.prettierrc.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/** @type {import('prettier').Config} */
|
||||||
|
module.exports = require('../../.prettierrc.json');
|
||||||
191
packages/structures/LICENSE
Normal file
191
packages/structures/LICENSE
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2023 Noel Buechler
|
||||||
|
Copyright 2023 Chai Kohen
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
69
packages/structures/README.md
Normal file
69
packages/structures/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<div align="center">
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/@discordjs/structures"><img src="https://img.shields.io/npm/v/@discordjs/structures.svg?maxAge=3600" alt="npm version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/@discordjs/structures"><img src="https://img.shields.io/npm/dt/@discordjs/structures.svg?maxAge=3600" alt="npm downloads" /></a>
|
||||||
|
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/tests.yml/badge.svg" alt="Tests status" /></a>
|
||||||
|
<a href="https://github.com/discordjs/discord.js/commits/main/packages/structures"><img alt="Last commit." src="https://img.shields.io/github/last-commit/discordjs/discord.js?logo=github&logoColor=ffffff&path=packages%2Fstructures" /></a>
|
||||||
|
<a href="https://codecov.io/gh/discordjs/discord.js"><img src="https://codecov.io/gh/discordjs/discord.js/branch/main/graph/badge.svg?precision=2&flag=structures" alt="Code coverage" /></a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-vercel.svg" alt="Vercel" /></a>
|
||||||
|
<a href="https://www.cloudflare.com"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-workers.png" alt="Cloudflare Workers" height="44" /></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
`@discordjs/structures` is a low level wrapper around Discord JSON Objects, meant to be a foundation to build upon in a higher level library.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
**Node.js 22.12.0 or newer is required.**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install @discordjs/structures
|
||||||
|
yarn add @discordjs/structures
|
||||||
|
pnpm add @discordjs/structures
|
||||||
|
bun add @discordjs/structures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Website][website] ([source][website-source])
|
||||||
|
- [Documentation][documentation]
|
||||||
|
- [Guide][guide] ([source][guide-source])
|
||||||
|
Also see the v13 to v14 [Update Guide][guide-update], which includes updated and removed items from the library.
|
||||||
|
- [discord.js Discord server][discord]
|
||||||
|
- [Discord Developers Discord server][discord-developers]
|
||||||
|
- [GitHub][source]
|
||||||
|
- [npm][npm]
|
||||||
|
- [Related libraries][related-libs]
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||||
|
[documentation][documentation].
|
||||||
|
See [the contribution guide][contributing] if you'd like to submit a PR.
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord].
|
||||||
|
|
||||||
|
[website]: https://discord.js.org
|
||||||
|
[website-source]: https://github.com/discordjs/discord.js/tree/main/apps/website
|
||||||
|
[documentation]: https://discord.js.org/docs/packages/structures/stable
|
||||||
|
[guide]: https://discordjs.guide/
|
||||||
|
[guide-source]: https://github.com/discordjs/guide
|
||||||
|
[guide-update]: https://discordjs.guide/additional-info/changes-in-v14.html
|
||||||
|
[discord]: https://discord.gg/djs
|
||||||
|
[discord-developers]: https://discord.gg/discord-developers
|
||||||
|
[source]: https://github.com/discordjs/discord.js/tree/main/packages/structures
|
||||||
|
[npm]: https://www.npmjs.com/package/@discordjs/structures
|
||||||
|
[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries
|
||||||
|
[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md
|
||||||
98
packages/structures/__tests__/Mixin.test.ts
Normal file
98
packages/structures/__tests__/Mixin.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import { kData, kPatch } from '../src/utils/symbols.js';
|
||||||
|
import type { APIData } from './mixinClasses.js';
|
||||||
|
import { Base, Mixed, MixedWithExtended } from './mixinClasses.js';
|
||||||
|
|
||||||
|
describe('Mixin function', () => {
|
||||||
|
const data: APIData = {
|
||||||
|
id: '1',
|
||||||
|
property1: 23,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Mixed class has all getters', () => {
|
||||||
|
const instance = new Mixed(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.property1).toBe(data.property1);
|
||||||
|
expect(instance.property2).toBe(data.property2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed class has all methods', () => {
|
||||||
|
const instance = new Mixed(data);
|
||||||
|
expect(instance.getId()).toBe(data.id);
|
||||||
|
expect(instance.getProperty1()).toBe(data.property1);
|
||||||
|
expect(instance.getProperty2()).toBe(data.property2);
|
||||||
|
expect(instance.getProperties()).toEqual({
|
||||||
|
property1: data.property1,
|
||||||
|
property2: data.property2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed with extended class has all getters', () => {
|
||||||
|
const instance = new MixedWithExtended(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.property1).toBe(data.property1);
|
||||||
|
expect(instance.property2).toBe(data.property2);
|
||||||
|
expect(instance.isExtended).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed with extended class has all methods', () => {
|
||||||
|
const instance = new MixedWithExtended(data);
|
||||||
|
expect(instance.getId()).toBe(data.id);
|
||||||
|
expect(instance.getProperty1()).toBe(data.property1);
|
||||||
|
expect(instance.getProperty2()).toBe(data.property2);
|
||||||
|
expect(instance.getProperties()).toEqual({
|
||||||
|
property1: data.property1,
|
||||||
|
property2: data.property2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed class calls construct methods on construct', () => {
|
||||||
|
const instance1 = new Mixed(data);
|
||||||
|
const instance2 = new MixedWithExtended(data);
|
||||||
|
expect(instance1.constructCalled).toBe(true);
|
||||||
|
expect(instance2.constructCalled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed class respects mixin data optimizations', () => {
|
||||||
|
expect(typeof Object.getOwnPropertyDescriptor(Mixed.DataTemplate, 'mixinOptimize')?.set).toBe('function');
|
||||||
|
const missingOptimizedInstance = new Mixed(data);
|
||||||
|
const alreadyOptimizedInstance = new Mixed({ ...data, mixinOptimize: 'true', baseOptimize: 'true' });
|
||||||
|
const baseOptimizedInstance = new Base({ ...data, mixinOptimize: 'true', baseOptimize: 'true' });
|
||||||
|
|
||||||
|
expect(missingOptimizedInstance.baseOptimize).toBe(null);
|
||||||
|
expect(missingOptimizedInstance.mixinOptimize).toBe(null);
|
||||||
|
// Setters pass this
|
||||||
|
expect('baseOptimize' in missingOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect('mixinOptimize' in missingOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect(missingOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||||
|
expect(missingOptimizedInstance[kData].mixinOptimize).toBeUndefined();
|
||||||
|
|
||||||
|
expect(alreadyOptimizedInstance.baseOptimize).toBe(true);
|
||||||
|
expect(alreadyOptimizedInstance.mixinOptimize).toBe(true);
|
||||||
|
// Setters pass this
|
||||||
|
expect('baseOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect('mixinOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect(alreadyOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||||
|
expect(alreadyOptimizedInstance[kData].mixinOptimize).toBeUndefined();
|
||||||
|
expect(alreadyOptimizedInstance.toJSON()).toEqual({ ...data, mixinOptimize: 'true', baseOptimize: 'true' });
|
||||||
|
|
||||||
|
alreadyOptimizedInstance[kPatch]({ mixinOptimize: '', baseOptimize: '' });
|
||||||
|
|
||||||
|
expect(alreadyOptimizedInstance.baseOptimize).toBe(false);
|
||||||
|
expect(alreadyOptimizedInstance.mixinOptimize).toBe(false);
|
||||||
|
// Setters pass this
|
||||||
|
expect('baseOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect('mixinOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect(alreadyOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||||
|
expect(alreadyOptimizedInstance[kData].mixinOptimize).toBeUndefined();
|
||||||
|
|
||||||
|
// Ensure mixin optimizations don't happen on base (ie overwritten DataTemplate)
|
||||||
|
expect(baseOptimizedInstance.baseOptimize).toBe(true);
|
||||||
|
expect('mixinOptimize' in baseOptimizedInstance).toBe(false);
|
||||||
|
// Setters pass this
|
||||||
|
expect('baseOptimize' in baseOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect('mixinOptimize' in baseOptimizedInstance[kData]).toBe(true);
|
||||||
|
expect(baseOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||||
|
expect(baseOptimizedInstance[kData].mixinOptimize).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
65
packages/structures/__tests__/Structure.test.ts
Normal file
65
packages/structures/__tests__/Structure.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, test, expect, beforeEach } from 'vitest';
|
||||||
|
import { DataTemplatePropertyName, OptimizeDataPropertyName, Structure } from '../src/Structure.js';
|
||||||
|
import { kData, kPatch } from '../src/utils/symbols.js';
|
||||||
|
|
||||||
|
describe('Base Structure', () => {
|
||||||
|
const data = { test: true, patched: false, removed: true };
|
||||||
|
let struct: Structure<typeof data>;
|
||||||
|
beforeEach(() => {
|
||||||
|
// @ts-expect-error Structure constructor is protected
|
||||||
|
struct = new Structure(data);
|
||||||
|
// @ts-expect-error Structure.DataTemplate is protected
|
||||||
|
Structure.DataTemplate = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Data reference is not identical (clone via Object.assign)', () => {
|
||||||
|
expect(struct[kData]).not.toBe(data);
|
||||||
|
expect(struct[kData]).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Remove properties via template (constructor)', () => {
|
||||||
|
// @ts-expect-error Structure.DataTemplate is protected
|
||||||
|
Structure.DataTemplate = { set removed(_) {} };
|
||||||
|
// @ts-expect-error Structure constructor is protected
|
||||||
|
const templatedStruct: Structure<typeof data> = new Structure(data);
|
||||||
|
expect(templatedStruct[kData].removed).toBe(undefined);
|
||||||
|
// Setters still exist and pass "in" test unfortunately
|
||||||
|
expect('removed' in templatedStruct[kData]).toBe(true);
|
||||||
|
expect(templatedStruct[kData]).toEqual({ test: true, patched: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('patch clones data and updates in place', () => {
|
||||||
|
const dataBefore = struct[kData];
|
||||||
|
const patched = struct[kPatch]({ patched: true });
|
||||||
|
expect(patched[kData].patched).toBe(true);
|
||||||
|
// Patch in place
|
||||||
|
expect(struct[kData]).toBe(patched[kData]);
|
||||||
|
// Clones
|
||||||
|
expect(dataBefore.patched).toBe(false);
|
||||||
|
expect(dataBefore).not.toBe(patched[kData]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Remove properties via template ([kPatch])', () => {
|
||||||
|
// @ts-expect-error Structure.DataTemplate is protected
|
||||||
|
Structure.DataTemplate = { set removed(_) {} };
|
||||||
|
// @ts-expect-error Structure constructor is protected
|
||||||
|
const templatedStruct: Structure<typeof data> = new Structure(data);
|
||||||
|
templatedStruct[kPatch]({ removed: false });
|
||||||
|
expect(templatedStruct[kData].removed).toBe(undefined);
|
||||||
|
// Setters still exist and pass "in" test unfortunately
|
||||||
|
expect('removed' in templatedStruct[kData]).toBe(true);
|
||||||
|
expect(templatedStruct[kData]).toEqual({ test: true, patched: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toJSON clones but retains data equality', () => {
|
||||||
|
const json = struct.toJSON();
|
||||||
|
expect(json).not.toBe(data);
|
||||||
|
expect(json).not.toBe(struct[kData]);
|
||||||
|
expect(struct[kData]).toEqual(json);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("XPropertyName variable matches the actual property's names", () => {
|
||||||
|
expect(Structure[DataTemplatePropertyName]).toStrictEqual({});
|
||||||
|
expect(struct[OptimizeDataPropertyName]).toBeTypeOf('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
741
packages/structures/__tests__/channels.test.ts
Normal file
741
packages/structures/__tests__/channels.test.ts
Normal file
@@ -0,0 +1,741 @@
|
|||||||
|
import type {
|
||||||
|
APIAnnouncementThreadChannel,
|
||||||
|
APIDMChannel,
|
||||||
|
APIGroupDMChannel,
|
||||||
|
APIGuildCategoryChannel,
|
||||||
|
APIGuildForumChannel,
|
||||||
|
APIGuildMediaChannel,
|
||||||
|
APIGuildStageVoiceChannel,
|
||||||
|
APIGuildVoiceChannel,
|
||||||
|
APINewsChannel,
|
||||||
|
APIPrivateThreadChannel,
|
||||||
|
APIPublicThreadChannel,
|
||||||
|
APITextChannel,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import {
|
||||||
|
ForumLayoutType,
|
||||||
|
SortOrderType,
|
||||||
|
ChannelType,
|
||||||
|
OverwriteType,
|
||||||
|
ThreadAutoArchiveDuration,
|
||||||
|
VideoQualityMode,
|
||||||
|
ChannelFlags,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import {
|
||||||
|
AnnouncementChannel,
|
||||||
|
AnnouncementThreadChannel,
|
||||||
|
CategoryChannel,
|
||||||
|
DMChannel,
|
||||||
|
ForumChannel,
|
||||||
|
ForumTag,
|
||||||
|
GroupDMChannel,
|
||||||
|
MediaChannel,
|
||||||
|
PermissionOverwrite,
|
||||||
|
PrivateThreadChannel,
|
||||||
|
PublicThreadChannel,
|
||||||
|
StageChannel,
|
||||||
|
TextChannel,
|
||||||
|
ThreadMetadata,
|
||||||
|
VoiceChannel,
|
||||||
|
} from '../src/index.js';
|
||||||
|
import { kData } from '../src/utils/symbols.js';
|
||||||
|
|
||||||
|
describe('text channel', () => {
|
||||||
|
const data: APITextChannel = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
last_message_id: '3',
|
||||||
|
last_pin_timestamp: '2020-10-10T13:50:17.209Z',
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
permission_overwrites: [
|
||||||
|
{
|
||||||
|
allow: '123',
|
||||||
|
deny: '456',
|
||||||
|
type: OverwriteType.Member,
|
||||||
|
id: '5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rate_limit_per_user: 9,
|
||||||
|
topic: 'hello',
|
||||||
|
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||||
|
default_thread_rate_limit_per_user: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('TextChannel has all properties', () => {
|
||||||
|
const instance = new TextChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||||
|
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||||
|
expect(instance.lastPinTimestamp).toBe(Date.parse(data.last_pin_timestamp!));
|
||||||
|
expect(instance.lastPinAt?.toISOString()).toBe(data.last_pin_timestamp);
|
||||||
|
expect(instance.nsfw).toBe(data.nsfw);
|
||||||
|
expect(instance.parentId).toBe(data.parent_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||||
|
expect(instance.topic).toBe(data.topic);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildText);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new TextChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PermissionOverwrite sub-structure', () => {
|
||||||
|
const instances = data.permission_overwrites?.map((overwrite) => new PermissionOverwrite(overwrite));
|
||||||
|
expect(instances?.map((overwrite) => overwrite.toJSON())).toEqual(data.permission_overwrites);
|
||||||
|
expect(instances?.[0]?.allow?.toJSON()).toBe(data.permission_overwrites?.[0]?.allow);
|
||||||
|
expect(instances?.[0]?.deny?.toJSON()).toBe(data.permission_overwrites?.[0]?.deny);
|
||||||
|
expect(instances?.[0]?.id).toBe(data.permission_overwrites?.[0]?.id);
|
||||||
|
expect(instances?.[0]?.type).toBe(data.permission_overwrites?.[0]?.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('announcement channel', () => {
|
||||||
|
const data: APINewsChannel = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildAnnouncement,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
last_message_id: '3',
|
||||||
|
last_pin_timestamp: null,
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
rate_limit_per_user: 9,
|
||||||
|
topic: 'hello',
|
||||||
|
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||||
|
default_thread_rate_limit_per_user: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('AnnouncementChannel has all properties', () => {
|
||||||
|
const instance = new AnnouncementChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||||
|
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||||
|
expect(instance.lastPinTimestamp).toBe(null);
|
||||||
|
expect(instance.lastPinAt).toBe(data.last_pin_timestamp);
|
||||||
|
expect(instance.nsfw).toBe(data.nsfw);
|
||||||
|
expect(instance.parentId).toBe(data.parent_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||||
|
expect(instance.topic).toBe(data.topic);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildAnnouncement);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new AnnouncementChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('category channel', () => {
|
||||||
|
const data: APIGuildCategoryChannel = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildCategory,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
permission_overwrites: [
|
||||||
|
{
|
||||||
|
allow: '123',
|
||||||
|
deny: '456',
|
||||||
|
type: OverwriteType.Member,
|
||||||
|
id: '5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('CategoryChannel has all properties', () => {
|
||||||
|
const instance = new CategoryChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildCategory);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new CategoryChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(false);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DM channel', () => {
|
||||||
|
const dataNoRecipients: APIDMChannel = {
|
||||||
|
id: '1',
|
||||||
|
type: ChannelType.DM,
|
||||||
|
last_message_id: '3',
|
||||||
|
last_pin_timestamp: '2020-10-10T13:50:17.209Z',
|
||||||
|
name: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...dataNoRecipients,
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
avatar: '123',
|
||||||
|
discriminator: '0',
|
||||||
|
global_name: 'tester',
|
||||||
|
id: '1',
|
||||||
|
username: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('DMChannel has all properties', () => {
|
||||||
|
const instance = new DMChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||||
|
expect(instance.lastPinTimestamp).toBe(Date.parse(data.last_pin_timestamp!));
|
||||||
|
expect(instance.lastPinAt?.toISOString()).toBe(data.last_pin_timestamp);
|
||||||
|
expect(instance[kData].recipients).toEqual(data.recipients);
|
||||||
|
expect(instance.type).toBe(ChannelType.DM);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/@me/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DMChannel with no recipients', () => {
|
||||||
|
const instance = new DMChannel(dataNoRecipients);
|
||||||
|
expect(instance[kData].recipients).toEqual(dataNoRecipients.recipients);
|
||||||
|
expect(instance.toJSON()).toEqual(dataNoRecipients);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new DMChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(true);
|
||||||
|
expect(instance.isGuildBased()).toBe(false);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(false);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GroupDM channel', () => {
|
||||||
|
const data: APIGroupDMChannel = {
|
||||||
|
id: '1',
|
||||||
|
type: ChannelType.GroupDM,
|
||||||
|
last_message_id: '3',
|
||||||
|
name: 'name',
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
avatar: '123',
|
||||||
|
discriminator: '0',
|
||||||
|
global_name: 'tester',
|
||||||
|
id: '1',
|
||||||
|
username: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
last_pin_timestamp: null,
|
||||||
|
application_id: '34',
|
||||||
|
icon: 'abc',
|
||||||
|
managed: true,
|
||||||
|
owner_id: '567',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('GroupDMChannel has all properties', () => {
|
||||||
|
const instance = new GroupDMChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||||
|
expect(instance[kData].recipients).toEqual(data.recipients);
|
||||||
|
expect(instance.applicationId).toBe(data.application_id);
|
||||||
|
expect(instance.managed).toBe(data.managed);
|
||||||
|
expect(instance.ownerId).toBe(data.owner_id);
|
||||||
|
expect(instance.type).toBe(ChannelType.GroupDM);
|
||||||
|
expect(instance.icon).toBe(data.icon);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/@me/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new GroupDMChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(true);
|
||||||
|
expect(instance.isGuildBased()).toBe(false);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(false);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forum channel', () => {
|
||||||
|
const dataNoTags: Omit<APIGuildForumChannel, 'available_tags'> = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildForum,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
permission_overwrites: [
|
||||||
|
{
|
||||||
|
allow: '123',
|
||||||
|
deny: '456',
|
||||||
|
type: OverwriteType.Member,
|
||||||
|
id: '5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
topic: 'hello',
|
||||||
|
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||||
|
default_thread_rate_limit_per_user: 30,
|
||||||
|
default_forum_layout: ForumLayoutType.GalleryView,
|
||||||
|
default_reaction_emoji: {
|
||||||
|
emoji_id: '159',
|
||||||
|
emoji_name: null,
|
||||||
|
},
|
||||||
|
default_sort_order: SortOrderType.LatestActivity,
|
||||||
|
};
|
||||||
|
const data: APIGuildForumChannel = {
|
||||||
|
...dataNoTags,
|
||||||
|
available_tags: [
|
||||||
|
{
|
||||||
|
name: 'emoji',
|
||||||
|
emoji_name: '😀',
|
||||||
|
moderated: false,
|
||||||
|
id: '789',
|
||||||
|
emoji_id: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('ForumChannel has all properties', () => {
|
||||||
|
const instance = new ForumChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||||
|
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance.nsfw).toBe(data.nsfw);
|
||||||
|
expect(instance.parentId).toBe(data.parent_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance.defaultForumLayout).toBe(data.default_forum_layout);
|
||||||
|
expect(instance.defaultReactionEmoji).toBe(data.default_reaction_emoji);
|
||||||
|
expect(instance.defaultSortOrder).toBe(data.default_sort_order);
|
||||||
|
expect(instance[kData].available_tags).toEqual(data.available_tags);
|
||||||
|
expect(instance.topic).toBe(data.topic);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildForum);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new ForumChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(false);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(true);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ForumTag has all properties', () => {
|
||||||
|
const instances = data.available_tags.map((tag) => new ForumTag(tag));
|
||||||
|
expect(instances.map((tag) => tag.toJSON())).toEqual(data.available_tags);
|
||||||
|
expect(instances[0]?.id).toBe(data.available_tags[0]?.id);
|
||||||
|
expect(instances[0]?.emojiId).toBe(data.available_tags[0]?.emoji_id);
|
||||||
|
expect(instances[0]?.emojiName).toBe(data.available_tags[0]?.emoji_name);
|
||||||
|
expect(instances[0]?.name).toBe(data.available_tags[0]?.name);
|
||||||
|
expect(instances[0]?.moderated).toBe(data.available_tags[0]?.moderated);
|
||||||
|
expect(instances[0]?.emoji).toBe(data.available_tags[0]?.emoji_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('omitted property from ForumChannel', () => {
|
||||||
|
const instance = new ForumChannel(dataNoTags);
|
||||||
|
expect(instance.toJSON()).toEqual(dataNoTags);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('media channel', () => {
|
||||||
|
const data: APIGuildMediaChannel = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildMedia,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
permission_overwrites: [
|
||||||
|
{
|
||||||
|
allow: '123',
|
||||||
|
deny: '456',
|
||||||
|
type: OverwriteType.Member,
|
||||||
|
id: '5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
topic: 'hello',
|
||||||
|
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||||
|
default_thread_rate_limit_per_user: 30,
|
||||||
|
available_tags: [
|
||||||
|
{
|
||||||
|
name: 'emoji',
|
||||||
|
emoji_name: null,
|
||||||
|
moderated: false,
|
||||||
|
id: '789',
|
||||||
|
emoji_id: '444',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default_reaction_emoji: {
|
||||||
|
emoji_id: '159',
|
||||||
|
emoji_name: null,
|
||||||
|
},
|
||||||
|
default_sort_order: SortOrderType.LatestActivity,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('MediaChannel has all properties', () => {
|
||||||
|
const instance = new MediaChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||||
|
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance.nsfw).toBe(data.nsfw);
|
||||||
|
expect(instance.parentId).toBe(data.parent_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance[kData].available_tags).toEqual(data.available_tags);
|
||||||
|
expect(instance.topic).toBe(data.topic);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildMedia);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new MediaChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(false);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(true);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ForumTag has all properties', () => {
|
||||||
|
const instances = data.available_tags.map((tag) => new ForumTag(tag));
|
||||||
|
expect(instances.map((tag) => tag.toJSON())).toEqual(data.available_tags);
|
||||||
|
expect(instances[0]?.emoji).toBe(`<:_:${data.available_tags[0]?.emoji_id}>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('voice channel', () => {
|
||||||
|
const data: APIGuildVoiceChannel = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildVoice,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
last_message_id: '3',
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
permission_overwrites: [
|
||||||
|
{
|
||||||
|
allow: '123',
|
||||||
|
deny: '456',
|
||||||
|
type: OverwriteType.Member,
|
||||||
|
id: '5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rate_limit_per_user: 9,
|
||||||
|
bitrate: 7,
|
||||||
|
rtc_region: 'somewhere',
|
||||||
|
user_limit: 100,
|
||||||
|
video_quality_mode: VideoQualityMode.Full,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('VoiceChannel has all properties', () => {
|
||||||
|
const instance = new VoiceChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.bitrate).toBe(data.bitrate);
|
||||||
|
expect(instance.rtcRegion).toBe(data.rtc_region);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||||
|
expect(instance.videoQualityMode).toBe(data.video_quality_mode);
|
||||||
|
expect(instance.userLimit).toBe(data.user_limit);
|
||||||
|
expect(instance.nsfw).toBe(data.nsfw);
|
||||||
|
expect(instance.parentId).toBe(data.parent_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildVoice);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new VoiceChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(true);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stage channel', () => {
|
||||||
|
const data: APIGuildStageVoiceChannel = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.GuildStageVoice,
|
||||||
|
position: 0,
|
||||||
|
guild_id: '2',
|
||||||
|
last_message_id: '3',
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
permission_overwrites: [
|
||||||
|
{
|
||||||
|
allow: '123',
|
||||||
|
deny: '456',
|
||||||
|
type: OverwriteType.Member,
|
||||||
|
id: '5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rate_limit_per_user: 9,
|
||||||
|
bitrate: 7,
|
||||||
|
rtc_region: 'somewhere',
|
||||||
|
user_limit: 100,
|
||||||
|
video_quality_mode: VideoQualityMode.Full,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('StageChannel has all properties', () => {
|
||||||
|
const instance = new StageChannel(data);
|
||||||
|
expect(instance.id).toBe(data.id);
|
||||||
|
expect(instance.name).toBe(data.name);
|
||||||
|
expect(instance.position).toBe(data.position);
|
||||||
|
expect(instance.bitrate).toBe(data.bitrate);
|
||||||
|
expect(instance.rtcRegion).toBe(data.rtc_region);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||||
|
expect(instance.guildId).toBe(data.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||||
|
expect(instance.videoQualityMode).toBe(data.video_quality_mode);
|
||||||
|
expect(instance.nsfw).toBe(data.nsfw);
|
||||||
|
expect(instance.parentId).toBe(data.parent_id);
|
||||||
|
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||||
|
expect(instance.type).toBe(ChannelType.GuildStageVoice);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards', () => {
|
||||||
|
const instance = new StageChannel(data);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(true);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(false);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(true);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('thread channels', () => {
|
||||||
|
const dataNoTags: Omit<APIPublicThreadChannel, 'applied_tags'> = {
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: ChannelType.PublicThread,
|
||||||
|
guild_id: '2',
|
||||||
|
last_message_id: '3',
|
||||||
|
last_pin_timestamp: null,
|
||||||
|
nsfw: true,
|
||||||
|
parent_id: '4',
|
||||||
|
rate_limit_per_user: 9,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataPublic: APIPublicThreadChannel = {
|
||||||
|
...dataNoTags,
|
||||||
|
applied_tags: ['567'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataAnnounce: APIAnnouncementThreadChannel = {
|
||||||
|
...dataPublic,
|
||||||
|
thread_metadata: {
|
||||||
|
archive_timestamp: '2024-09-08T12:01:02.345Z',
|
||||||
|
archived: false,
|
||||||
|
auto_archive_duration: ThreadAutoArchiveDuration.ThreeDays,
|
||||||
|
locked: true,
|
||||||
|
},
|
||||||
|
flags: ChannelFlags.Pinned,
|
||||||
|
type: ChannelType.AnnouncementThread,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataPrivate: APIPrivateThreadChannel = {
|
||||||
|
...dataPublic,
|
||||||
|
thread_metadata: {
|
||||||
|
...dataAnnounce.thread_metadata!,
|
||||||
|
create_timestamp: '2023-01-02T15:13:11.987Z',
|
||||||
|
invitable: true,
|
||||||
|
},
|
||||||
|
type: ChannelType.PrivateThread,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('PublicThreadChannel has all properties', () => {
|
||||||
|
const instance = new PublicThreadChannel(dataPublic);
|
||||||
|
expect(instance.id).toBe(dataPublic.id);
|
||||||
|
expect(instance.name).toBe(dataPublic.name);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(dataPublic.flags);
|
||||||
|
expect(instance.guildId).toBe(dataPublic.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(dataPublic.last_message_id);
|
||||||
|
expect(instance.nsfw).toBe(dataPublic.nsfw);
|
||||||
|
expect(instance.parentId).toBe(dataPublic.parent_id);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(dataPublic.rate_limit_per_user);
|
||||||
|
expect(instance.type).toBe(ChannelType.PublicThread);
|
||||||
|
expect(instance.appliedTags).toEqual(dataPublic.applied_tags);
|
||||||
|
expect(instance.memberCount).toBe(dataPublic.member_count);
|
||||||
|
expect(instance.messageCount).toBe(dataPublic.message_count);
|
||||||
|
expect(instance.totalMessageSent).toBe(dataPublic.total_message_sent);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(dataPublic);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards PublicThread', () => {
|
||||||
|
const instance = new PublicThreadChannel(dataPublic);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(false);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(true);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PrivateThreadChannel has all properties', () => {
|
||||||
|
const instance = new PrivateThreadChannel(dataPrivate);
|
||||||
|
expect(instance.id).toBe(dataPrivate.id);
|
||||||
|
expect(instance.name).toBe(dataPrivate.name);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(dataPrivate.flags);
|
||||||
|
expect(instance.guildId).toBe(dataPrivate.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(dataPrivate.last_message_id);
|
||||||
|
expect(instance.nsfw).toBe(dataPrivate.nsfw);
|
||||||
|
expect(instance.parentId).toBe(dataPrivate.parent_id);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(dataPrivate.rate_limit_per_user);
|
||||||
|
expect(instance[kData].thread_metadata).toEqual(dataPrivate.thread_metadata);
|
||||||
|
expect(instance.type).toBe(ChannelType.PrivateThread);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(dataPrivate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards PrivateThread', () => {
|
||||||
|
const instance = new PrivateThreadChannel(dataPrivate);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(false);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(true);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AnnouncementThreadChannel has all properties', () => {
|
||||||
|
const instance = new AnnouncementThreadChannel(dataAnnounce);
|
||||||
|
expect(instance.id).toBe(dataAnnounce.id);
|
||||||
|
expect(instance.name).toBe(dataAnnounce.name);
|
||||||
|
expect(instance.flags?.toJSON()).toBe(dataAnnounce.flags);
|
||||||
|
expect(instance.guildId).toBe(dataAnnounce.guild_id);
|
||||||
|
expect(instance.lastMessageId).toBe(dataAnnounce.last_message_id);
|
||||||
|
expect(instance.nsfw).toBe(dataAnnounce.nsfw);
|
||||||
|
expect(instance.parentId).toBe(dataAnnounce.parent_id);
|
||||||
|
expect(instance.rateLimitPerUser).toBe(dataAnnounce.rate_limit_per_user);
|
||||||
|
expect(instance[kData].thread_metadata).toEqual(dataAnnounce.thread_metadata);
|
||||||
|
expect(instance.type).toBe(ChannelType.AnnouncementThread);
|
||||||
|
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||||
|
expect(instance.toJSON()).toEqual(dataAnnounce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guards AnnouncementThread', () => {
|
||||||
|
const instance = new AnnouncementThreadChannel(dataAnnounce);
|
||||||
|
expect(instance.isDMBased()).toBe(false);
|
||||||
|
expect(instance.isGuildBased()).toBe(true);
|
||||||
|
expect(instance.isPermissionCapable()).toBe(false);
|
||||||
|
expect(instance.isTextBased()).toBe(true);
|
||||||
|
expect(instance.isThread()).toBe(true);
|
||||||
|
expect(instance.isThreadOnly()).toBe(false);
|
||||||
|
expect(instance.isVoiceBased()).toBe(false);
|
||||||
|
expect(instance.isWebhookCapable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('omitted property from PublicThread', () => {
|
||||||
|
const instance = new PublicThreadChannel(dataNoTags);
|
||||||
|
expect(instance.toJSON()).toEqual(dataNoTags);
|
||||||
|
expect(instance.appliedTags).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ThreadMetadata has all properties', () => {
|
||||||
|
const instance = new ThreadMetadata(dataPrivate.thread_metadata!);
|
||||||
|
expect(instance.toJSON()).toEqual(dataPrivate.thread_metadata);
|
||||||
|
expect(instance.archived).toBe(dataPrivate.thread_metadata?.archived);
|
||||||
|
expect(instance.archivedAt?.toISOString()).toBe(dataPrivate.thread_metadata?.archive_timestamp);
|
||||||
|
expect(instance.archivedTimestamp).toBe(Date.parse(dataPrivate.thread_metadata!.archive_timestamp));
|
||||||
|
expect(instance.createdAt?.toISOString()).toBe(dataPrivate.thread_metadata?.create_timestamp);
|
||||||
|
expect(instance.createdTimestamp).toBe(Date.parse(dataPrivate.thread_metadata!.create_timestamp!));
|
||||||
|
expect(instance.autoArchiveDuration).toBe(dataPrivate.thread_metadata?.auto_archive_duration);
|
||||||
|
expect(instance.invitable).toBe(dataPrivate.thread_metadata?.invitable);
|
||||||
|
expect(instance.locked).toBe(dataPrivate.thread_metadata?.locked);
|
||||||
|
});
|
||||||
|
});
|
||||||
90
packages/structures/__tests__/invite.test.ts
Normal file
90
packages/structures/__tests__/invite.test.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import type { APIExtendedInvite, APIInvite } from 'discord-api-types/v10';
|
||||||
|
import { InviteTargetType, InviteType } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { Invite } from '../src/index.js';
|
||||||
|
import { kPatch } from '../src/utils/symbols.js';
|
||||||
|
|
||||||
|
describe('Invite', () => {
|
||||||
|
const dataNoCode: Omit<APIInvite, 'code'> = {
|
||||||
|
type: InviteType.Guild,
|
||||||
|
channel: null,
|
||||||
|
approximate_member_count: 15,
|
||||||
|
approximate_presence_count: 35,
|
||||||
|
target_type: InviteTargetType.EmbeddedApplication,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data: APIInvite = {
|
||||||
|
...dataNoCode,
|
||||||
|
code: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataExtended: APIExtendedInvite = {
|
||||||
|
...data,
|
||||||
|
created_at: '2020-10-10T13:50:17.209Z',
|
||||||
|
max_age: 12,
|
||||||
|
max_uses: 34,
|
||||||
|
temporary: false,
|
||||||
|
uses: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Invite has all properties', () => {
|
||||||
|
const instance = new Invite(data);
|
||||||
|
expect(instance.type).toBe(data.type);
|
||||||
|
expect(instance.code).toBe(data.code);
|
||||||
|
expect(instance.createdAt).toBe(null);
|
||||||
|
expect(instance.createdTimestamp).toBe(null);
|
||||||
|
expect(instance.maxAge).toBe(undefined);
|
||||||
|
expect(instance.maxUses).toBe(undefined);
|
||||||
|
expect(instance.approximateMemberCount).toBe(data.approximate_member_count);
|
||||||
|
expect(instance.approximatePresenceCount).toBe(data.approximate_presence_count);
|
||||||
|
expect(instance.targetType).toBe(data.target_type);
|
||||||
|
expect(instance.temporary).toBe(undefined);
|
||||||
|
expect(instance.uses).toBe(undefined);
|
||||||
|
expect(instance.expiresTimestamp).toBe(null);
|
||||||
|
expect(instance.expiresAt).toBe(null);
|
||||||
|
expect(instance.url).toBe('https://discord.gg/123');
|
||||||
|
expect(instance.toJSON()).toEqual(data);
|
||||||
|
expect(`${instance}`).toBe('https://discord.gg/123');
|
||||||
|
expect(instance.valueOf()).toBe(data.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extended Invite has all properties', () => {
|
||||||
|
const instance = new Invite(dataExtended);
|
||||||
|
expect(instance.type).toBe(data.type);
|
||||||
|
expect(instance.code).toBe(dataExtended.code);
|
||||||
|
expect(instance.createdAt?.toISOString()).toBe(dataExtended.created_at);
|
||||||
|
expect(instance.createdTimestamp).toBe(Date.parse(dataExtended.created_at));
|
||||||
|
expect(instance.maxAge).toBe(dataExtended.max_age);
|
||||||
|
expect(instance.maxUses).toBe(dataExtended.max_uses);
|
||||||
|
expect(instance.approximateMemberCount).toBe(dataExtended.approximate_member_count);
|
||||||
|
expect(instance.approximatePresenceCount).toBe(dataExtended.approximate_presence_count);
|
||||||
|
expect(instance.targetType).toBe(dataExtended.target_type);
|
||||||
|
expect(instance.temporary).toBe(dataExtended.temporary);
|
||||||
|
expect(instance.uses).toBe(dataExtended.uses);
|
||||||
|
expect(instance.expiresTimestamp).toStrictEqual(Date.parse('2020-10-10T13:50:29.209Z'));
|
||||||
|
expect(instance.expiresAt).toStrictEqual(new Date('2020-10-10T13:50:29.209Z'));
|
||||||
|
expect(instance.url).toBe('https://discord.gg/123');
|
||||||
|
expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invite with omitted properties', () => {
|
||||||
|
const instance = new Invite(dataNoCode);
|
||||||
|
expect(instance.toJSON()).toEqual(dataNoCode);
|
||||||
|
expect(instance.url).toBe(null);
|
||||||
|
expect(instance.code).toBe(undefined);
|
||||||
|
expect(`${instance}`).toBe('');
|
||||||
|
expect(instance.valueOf()).toEqual(Object.prototype.valueOf.apply(instance));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invite with expiration', () => {
|
||||||
|
const instance = new Invite({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
|
||||||
|
expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Patching Invite works in place', () => {
|
||||||
|
const instance1 = new Invite(data);
|
||||||
|
const instance2 = instance1[kPatch]({ max_age: 34 });
|
||||||
|
expect(instance1.toJSON()).not.toEqual(data);
|
||||||
|
expect(instance2).toBe(instance1);
|
||||||
|
});
|
||||||
|
});
|
||||||
132
packages/structures/__tests__/mixinClasses.ts
Normal file
132
packages/structures/__tests__/mixinClasses.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Mixin } from '../src/Mixin.js';
|
||||||
|
import type { MixinTypes } from '../src/MixinTypes.d.ts';
|
||||||
|
import { Structure } from '../src/Structure.js';
|
||||||
|
import { kData, kMixinConstruct, kMixinToJSON, kPatch } from '../src/utils/symbols.js';
|
||||||
|
|
||||||
|
export interface APIData {
|
||||||
|
baseOptimize?: string;
|
||||||
|
id: string;
|
||||||
|
mixinOptimize?: string;
|
||||||
|
property1?: number;
|
||||||
|
property2?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Base<Omitted extends keyof APIData | '' = ''> extends Structure<APIData, Omitted> {
|
||||||
|
public static override readonly DataTemplate = {
|
||||||
|
set baseOptimize(_: unknown) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public baseOptimize: boolean | null = null;
|
||||||
|
|
||||||
|
public constructor(data: APIData) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override [kPatch](data: Partial<APIData>) {
|
||||||
|
super[kPatch](data);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override optimizeData(data: Partial<APIData>) {
|
||||||
|
if ('baseOptimize' in data) {
|
||||||
|
this.baseOptimize = Boolean(data.baseOptimize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id() {
|
||||||
|
return this[kData].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override toJSON() {
|
||||||
|
const data = super.toJSON();
|
||||||
|
if (this.baseOptimize) {
|
||||||
|
data.baseOptimize = String(this.baseOptimize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MixinProperty1<Omitted extends keyof APIData | '' = ''> extends Base<Omitted> {
|
||||||
|
mixinOptimize: boolean | null;
|
||||||
|
}
|
||||||
|
export class MixinProperty1 {
|
||||||
|
public static readonly DataTemplate = {
|
||||||
|
set mixinOptimize(_: unknown) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public [kMixinConstruct]() {
|
||||||
|
this.mixinOptimize = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public optimizeData(data: Partial<APIData>) {
|
||||||
|
if ('mixinOptimize' in data) {
|
||||||
|
this.mixinOptimize = Boolean(data.mixinOptimize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get property1() {
|
||||||
|
return this[kData].property1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProperty1() {
|
||||||
|
return this.property1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [kMixinToJSON](data: Partial<APIData>) {
|
||||||
|
if (this.mixinOptimize) {
|
||||||
|
data.mixinOptimize = String(this.mixinOptimize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MixinProperty2<Omitted extends keyof APIData | '' = ''> extends Base<Omitted> {
|
||||||
|
constructCalled: boolean;
|
||||||
|
}
|
||||||
|
export class MixinProperty2 {
|
||||||
|
public [kMixinConstruct]() {
|
||||||
|
this.constructCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get property2() {
|
||||||
|
return this[kData].property2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProperty2() {
|
||||||
|
return this.property2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExtendedMixinProperty2 extends MixinProperty2 {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
||||||
|
public get isExtended() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Mixed extends MixinTypes<Base, [MixinProperty1, MixinProperty2]> {}
|
||||||
|
export class Mixed extends Base {
|
||||||
|
public getProperties() {
|
||||||
|
return { property1: this.property1, property2: this.property2 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(Mixed, [MixinProperty1, MixinProperty2]);
|
||||||
|
|
||||||
|
export interface MixedWithExtended extends MixinTypes<Base, [MixinProperty1, ExtendedMixinProperty2]> {}
|
||||||
|
export class MixedWithExtended extends Base {
|
||||||
|
public getProperties() {
|
||||||
|
return {
|
||||||
|
property1: this.property1,
|
||||||
|
property2: this.property2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentionally don't directly mix Property 2
|
||||||
|
Mixin(MixedWithExtended, [MixinProperty1, ExtendedMixinProperty2]);
|
||||||
40
packages/structures/__tests__/types/Mixin.test-d.ts
Normal file
40
packages/structures/__tests__/types/Mixin.test-d.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { expectNotType, expectType } from 'tsd';
|
||||||
|
import { expectTypeOf } from 'vitest';
|
||||||
|
import type { MixinTypes } from '../../src/MixinTypes.d.ts';
|
||||||
|
import type { kMixinConstruct } from '../../src/utils/symbols.js';
|
||||||
|
import type { MixinProperty1, Base, MixinProperty2 } from '../mixinClasses.js';
|
||||||
|
|
||||||
|
declare const extendsNoOmit: Omit<MixinProperty1, keyof Base | typeof kMixinConstruct>;
|
||||||
|
declare const extendsOmitProperty1: Omit<MixinProperty1<'property1'>, keyof Base | typeof kMixinConstruct>;
|
||||||
|
declare const extendsBothNoOmit: Omit<MixinProperty1 & MixinProperty2, keyof Base | typeof kMixinConstruct>;
|
||||||
|
declare const extendsBothOmitProperty1: Omit<
|
||||||
|
MixinProperty1<'property1'> & MixinProperty2<'property1'>,
|
||||||
|
keyof Base | typeof kMixinConstruct
|
||||||
|
>;
|
||||||
|
declare const extendsBothOmitBoth: Omit<
|
||||||
|
MixinProperty1<'property1'> & MixinProperty2<'property2'>,
|
||||||
|
keyof Base | typeof kMixinConstruct
|
||||||
|
>;
|
||||||
|
|
||||||
|
expectType<MixinTypes<Base, [MixinProperty1]>>(extendsNoOmit);
|
||||||
|
expectType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>]>>(extendsOmitProperty1);
|
||||||
|
expectNotType<MixinTypes<Base, [MixinProperty1]>>(extendsOmitProperty1);
|
||||||
|
expectNotType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>]>>(extendsNoOmit);
|
||||||
|
|
||||||
|
expectType<MixinTypes<Base, [MixinProperty1, MixinProperty2]>>(extendsBothNoOmit);
|
||||||
|
// Since MixinProperty2 doesn't utilize the type of property1 in kData, this works and is ok
|
||||||
|
expectType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>, MixinProperty2]>>(extendsBothOmitProperty1);
|
||||||
|
expectNotType<MixinTypes<Base, [MixinProperty1, MixinProperty2]>>(extendsBothOmitProperty1);
|
||||||
|
// Since MixinProperty2 doesn't utilize the type of property1 in kData, this works and is ok
|
||||||
|
expectNotType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>, MixinProperty2]>>(extendsBothNoOmit);
|
||||||
|
|
||||||
|
// Earlier mixins in the list must specify all properties because of the way merging works
|
||||||
|
expectType<
|
||||||
|
MixinTypes<Base<'property1' | 'property2'>, [MixinProperty1<'property1' | 'property2'>, MixinProperty2<'property2'>]>
|
||||||
|
>(extendsBothOmitBoth);
|
||||||
|
|
||||||
|
expectTypeOf<MixinTypes<Base<'property1'>, [MixinProperty1]>>().toBeNever();
|
||||||
|
// @ts-expect-error Shouldn't be able to assign non identical omits
|
||||||
|
expectTypeOf<MixinTypes<Base, [MixinProperty1<'property1'>]>>()
|
||||||
|
// Separate line so ts-expect-error doesn't match this ever
|
||||||
|
.toBeNever();
|
||||||
79
packages/structures/__tests__/types/channels.test-d.ts
Normal file
79
packages/structures/__tests__/types/channels.test-d.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { ChannelType, GuildChannelType, GuildTextChannelType, ThreadChannelType } from 'discord-api-types/v10';
|
||||||
|
import { expectNever, expectType } from 'tsd';
|
||||||
|
import type { Channel } from '../../src/index.js';
|
||||||
|
|
||||||
|
declare const channel: Channel;
|
||||||
|
|
||||||
|
if (channel.isGuildBased()) {
|
||||||
|
expectType<string>(channel.guildId);
|
||||||
|
expectType<GuildChannelType>(channel.type);
|
||||||
|
|
||||||
|
if (channel.isDMBased()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isPermissionCapable()) {
|
||||||
|
expectType<Exclude<GuildChannelType, ChannelType.GuildDirectory | ThreadChannelType>>(channel.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isTextBased()) {
|
||||||
|
expectType<GuildTextChannelType>(channel.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isWebhookCapable()) {
|
||||||
|
expectType<ChannelType.GuildForum | ChannelType.GuildMedia | Exclude<GuildTextChannelType, ThreadChannelType>>(
|
||||||
|
channel.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isThread()) {
|
||||||
|
expectType<ThreadChannelType>(channel.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isThreadOnly()) {
|
||||||
|
expectType<ChannelType.GuildForum | ChannelType.GuildMedia>(channel.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isVoiceBased()) {
|
||||||
|
expectType<ChannelType.GuildStageVoice | ChannelType.GuildVoice>(channel.type);
|
||||||
|
if (!channel.isTextBased()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel.isWebhookCapable()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isDMBased()) {
|
||||||
|
expectType<ChannelType.DM | ChannelType.GroupDM>(channel.type);
|
||||||
|
|
||||||
|
if (channel.isGuildBased()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isPermissionCapable()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isWebhookCapable()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isVoiceBased()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isThread()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isThreadOnly()) {
|
||||||
|
expectNever(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isTextBased()) {
|
||||||
|
expectType<ChannelType.DM | ChannelType.GroupDM>(channel.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/structures/api-extractor.json
Normal file
11
packages/structures/api-extractor.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../api-extractor.json",
|
||||||
|
"docModel": {
|
||||||
|
"projectFolderUrl": "https://github.com/discordjs/discord.js/tree/main/packages/structures"
|
||||||
|
},
|
||||||
|
"compiler": {
|
||||||
|
"overrideTsconfig": {
|
||||||
|
"exclude": ["src/**/*.d.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
packages/structures/cliff.toml
Normal file
79
packages/structures/cliff.toml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
[changelog]
|
||||||
|
header = """
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.\n
|
||||||
|
"""
|
||||||
|
body = """
|
||||||
|
{%- macro remote_url() -%}
|
||||||
|
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
{% if version %}\
|
||||||
|
# [{{ version | trim_start_matches(pat="v") }}]\
|
||||||
|
{% if previous %}\
|
||||||
|
{% if previous.version %}\
|
||||||
|
({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
|
||||||
|
{% else %}\
|
||||||
|
({{ self::remote_url() }}/tree/{{ version }})\
|
||||||
|
{% endif %}\
|
||||||
|
{% endif %} \
|
||||||
|
- ({{ timestamp | date(format="%Y-%m-%d") }})
|
||||||
|
{% else %}\
|
||||||
|
# [unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
## {{ group | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.scope %}\
|
||||||
|
**{{commit.scope}}:** \
|
||||||
|
{% endif %}\
|
||||||
|
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
|
||||||
|
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
|
||||||
|
{% if commit.breaking %}\
|
||||||
|
{% for footer in commit.footers %}\
|
||||||
|
{% if footer.breaking %}\
|
||||||
|
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
|
||||||
|
{% endif %}\
|
||||||
|
{% endfor %}\
|
||||||
|
{% endif %}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\
|
||||||
|
{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
|
||||||
|
\n### New Contributors\n
|
||||||
|
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
|
||||||
|
* @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
|
||||||
|
{% endfor %}\
|
||||||
|
{% endif %}\n
|
||||||
|
"""
|
||||||
|
trim = true
|
||||||
|
footer = ""
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = true
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "Features"},
|
||||||
|
{ message = "^fix", group = "Bug Fixes"},
|
||||||
|
{ message = "^docs", group = "Documentation"},
|
||||||
|
{ message = "^perf", group = "Performance"},
|
||||||
|
{ message = "^refactor", group = "Refactor"},
|
||||||
|
{ message = "^types", group = "Typings"},
|
||||||
|
{ message = ".*deprecated", body = ".*deprecated", group = "Deprecation"},
|
||||||
|
{ message = "^revert", skip = true},
|
||||||
|
{ message = "^style", group = "Styling"},
|
||||||
|
{ message = "^test", group = "Testing"},
|
||||||
|
{ message = "^chore", skip = true},
|
||||||
|
{ message = "^ci", skip = true},
|
||||||
|
{ message = "^build", skip = true},
|
||||||
|
{ body = ".*security", group = "Security"},
|
||||||
|
]
|
||||||
|
filter_commits = true
|
||||||
|
protect_breaking_commits = true
|
||||||
|
tag_pattern = "@discordjs/structures@[0-9]*"
|
||||||
|
ignore_tags = ""
|
||||||
|
topo_order = false
|
||||||
|
sort_commits = "newest"
|
||||||
|
|
||||||
|
[remote.github]
|
||||||
|
owner = "discordjs"
|
||||||
|
repo = "discord.js"
|
||||||
1
packages/structures/docs/README.md
Normal file
1
packages/structures/docs/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
## [View the documentation here.](https://discord.js.org/docs/packages/structures/main)
|
||||||
98
packages/structures/package.json
Normal file
98
packages/structures/package.json
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@discordjs/structures",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Wrapper around Discord's structures",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit && tsup",
|
||||||
|
"build:docs": "tsc -p tsconfig.docs.json && cpy \"./src/*.d.ts\" \"./dist-docs\"",
|
||||||
|
"test": "vitest run --config ../../vitest.config.ts",
|
||||||
|
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src",
|
||||||
|
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src",
|
||||||
|
"fmt": "pnpm run format",
|
||||||
|
"docs": "pnpm run build:docs && api-extractor run --local --minify && generate-split-documentation",
|
||||||
|
"prepack": "pnpm run build && pnpm run lint",
|
||||||
|
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/structures/*'",
|
||||||
|
"release": "cliff-jumper"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"directories": {
|
||||||
|
"lib": "src",
|
||||||
|
"test": "__tests__"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"Crawl <icrawltogo@gmail.com>",
|
||||||
|
"SpaceEEC <spaceeec@yahoo.com>",
|
||||||
|
"Vlad Frangu <me@vladfrangu.dev>",
|
||||||
|
"Aura Román <kyradiscord@gmail.com>",
|
||||||
|
"Chai Kohen <chaikohen@gmail.com>"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": [
|
||||||
|
"discord",
|
||||||
|
"api",
|
||||||
|
"discordapp",
|
||||||
|
"discordjs"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/discordjs/discord.js.git",
|
||||||
|
"directory": "packages/structures"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://discord.js.org",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/formatters": "workspace:^",
|
||||||
|
"@sapphire/snowflake": "^3.5.5",
|
||||||
|
"discord-api-types": "^0.38.15"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@discordjs/api-extractor": "workspace:^",
|
||||||
|
"@discordjs/scripts": "workspace:^",
|
||||||
|
"@favware/cliff-jumper": "^4.1.0",
|
||||||
|
"@types/node": "^22.15.2",
|
||||||
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
|
"cpy-cli": "^5.0.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"esbuild-plugin-version-injector": "^1.2.1",
|
||||||
|
"eslint": "^9.25.1",
|
||||||
|
"eslint-config-neon": "^0.2.7",
|
||||||
|
"eslint-formatter-compact": "^8.40.0",
|
||||||
|
"eslint-formatter-pretty": "^6.0.1",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"tsd": "^0.31.2",
|
||||||
|
"tsup": "^8.4.0",
|
||||||
|
"turbo": "^2.5.2",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vitest": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"provenance": true
|
||||||
|
},
|
||||||
|
"tsd": {
|
||||||
|
"directory": "__tests__/types"
|
||||||
|
}
|
||||||
|
}
|
||||||
179
packages/structures/src/Mixin.ts
Normal file
179
packages/structures/src/Mixin.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { DataTemplatePropertyName, OptimizeDataPropertyName, type Structure } from './Structure.js';
|
||||||
|
import { kMixinConstruct, kMixinToJSON } from './utils/symbols.js';
|
||||||
|
|
||||||
|
export type Mixinable<ClassType> = new (...args: unknown[]) => ClassType;
|
||||||
|
|
||||||
|
export type MixinBase<BaseClass extends Structure<{}>> =
|
||||||
|
BaseClass extends Structure<infer DataType, infer Omitted> ? Structure<DataType, Omitted> : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the prototype (getters, setters, and methods) of all mixins to the destination class.
|
||||||
|
* For type information see {@link MixinTypes}
|
||||||
|
*
|
||||||
|
* @param destination - The class to apply the mixins to, must extend the base that the mixins expect it to.
|
||||||
|
* @param mixins - Classes that contain "pure" prototypes to be copied on top of the destination class prototype
|
||||||
|
* @remarks All mixins should be "pure" in that they only contain getters, setters, and methods.
|
||||||
|
* The runtime code will only copy these, and adding properties to the class only results
|
||||||
|
* in the types of the mixed class being wrong.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* // Interface merging on the mixin to give type access to props on the base and kData that are available once copied
|
||||||
|
* interface TextMixin extends Channel {}
|
||||||
|
* class TextMixin {
|
||||||
|
* // Methods / getters
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Interface merging on the mixed class to give it accurate type information within the declaration and when instantiated
|
||||||
|
* interface TextChannel extends MixinTypes<Channel, [TextMixin]> {}
|
||||||
|
* class TextChannel extends Channel {}
|
||||||
|
*
|
||||||
|
* // Apply for runtime
|
||||||
|
* Mixin(TextChannel, [TextMixin])
|
||||||
|
* ```
|
||||||
|
* @typeParam DestinationClass - The class to be mixed, ensures that the mixins provided can be used with this destination
|
||||||
|
*/
|
||||||
|
export function Mixin<DestinationClass extends typeof Structure<{}>>(
|
||||||
|
destination: DestinationClass,
|
||||||
|
mixins: Mixinable<MixinBase<DestinationClass['prototype']>>[],
|
||||||
|
) {
|
||||||
|
const dataTemplates: Record<string, unknown>[] = [];
|
||||||
|
const dataOptimizations: ((data: unknown) => void)[] = [];
|
||||||
|
const enrichToJSONs: ((data: Partial<unknown>) => void)[] = [];
|
||||||
|
const constructors: ((data: Partial<unknown>) => void)[] = [];
|
||||||
|
|
||||||
|
for (const mixin of mixins) {
|
||||||
|
// The entire prototype chain, in reverse order, since we want to copy it all
|
||||||
|
const prototypeChain: MixinBase<DestinationClass['prototype']>[] = [];
|
||||||
|
let extendedClass = mixin;
|
||||||
|
while (extendedClass.prototype !== undefined) {
|
||||||
|
if (
|
||||||
|
DataTemplatePropertyName in extendedClass &&
|
||||||
|
typeof extendedClass.DataTemplate === 'object' &&
|
||||||
|
// eslint-disable-next-line no-eq-null, eqeqeq
|
||||||
|
extendedClass.DataTemplate != null
|
||||||
|
) {
|
||||||
|
dataTemplates.push(extendedClass.DataTemplate as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
|
||||||
|
prototypeChain.unshift(extendedClass.prototype);
|
||||||
|
extendedClass = Object.getPrototypeOf(extendedClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prototype of prototypeChain) {
|
||||||
|
// Symboled data isn't traversed by Object.entries, we can handle it here
|
||||||
|
if (prototype[kMixinConstruct]) {
|
||||||
|
constructors.push(prototype[kMixinConstruct]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prototype[kMixinToJSON]) {
|
||||||
|
enrichToJSONs.push(prototype[kMixinToJSON]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy instance methods and setters / getters
|
||||||
|
const originalDescriptors = Object.getOwnPropertyDescriptors(prototype);
|
||||||
|
const usingDescriptors: { [prop: string]: PropertyDescriptor } = {};
|
||||||
|
for (const [prop, descriptor] of Object.entries(originalDescriptors)) {
|
||||||
|
// Drop constructor
|
||||||
|
if (['constructor'].includes(prop)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for optimize function, we want to combine these
|
||||||
|
if (prop === OptimizeDataPropertyName) {
|
||||||
|
if (typeof descriptor.value !== 'function')
|
||||||
|
throw new RangeError(`Expected ${prop} to be a function, received ${typeof descriptor.value} instead.`);
|
||||||
|
dataOptimizations.push(descriptor.value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shouldn't be anything other than these without being instantiated, but just in case
|
||||||
|
if (
|
||||||
|
typeof descriptor.get !== 'undefined' ||
|
||||||
|
typeof descriptor.set !== 'undefined' ||
|
||||||
|
typeof descriptor.value === 'function'
|
||||||
|
) {
|
||||||
|
usingDescriptors[prop] = descriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperties(destination.prototype, usingDescriptors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the function to call any mixed constructors
|
||||||
|
if (constructors.length > 0) {
|
||||||
|
Object.defineProperty(destination.prototype, kMixinConstruct, {
|
||||||
|
writable: true,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
// eslint-disable-next-line func-name-matching
|
||||||
|
value: function _mixinConstructors(data: Partial<unknown>) {
|
||||||
|
for (const construct of constructors) {
|
||||||
|
construct.call(this, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all optimizations into a single function
|
||||||
|
const baseOptimize = Object.getOwnPropertyDescriptor(destination, OptimizeDataPropertyName);
|
||||||
|
if (baseOptimize && typeof baseOptimize.value === 'function') {
|
||||||
|
// call base last (mimic constructor behavior)
|
||||||
|
dataOptimizations.push(baseOptimize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const superOptimize = Object.getOwnPropertyDescriptor(
|
||||||
|
Object.getPrototypeOf(destination).prototype,
|
||||||
|
OptimizeDataPropertyName,
|
||||||
|
);
|
||||||
|
// the mixin base optimize should call super, so we can ignore the super in that case
|
||||||
|
if (!baseOptimize && superOptimize && typeof superOptimize.value === 'function') {
|
||||||
|
// call super first (mimic constructor behavior)
|
||||||
|
dataOptimizations.unshift(superOptimize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's more than one optimization or if there's an optimization that isn't on the destination (base)
|
||||||
|
if (dataOptimizations.length > 1 || (dataOptimizations.length === 1 && !baseOptimize)) {
|
||||||
|
Object.defineProperty(destination.prototype, OptimizeDataPropertyName, {
|
||||||
|
writable: true,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
// eslint-disable-next-line func-name-matching
|
||||||
|
value: function _mixinOptimizeData(data: unknown) {
|
||||||
|
for (const optimization of dataOptimizations) {
|
||||||
|
optimization.call(this, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enrichToJSONs.length > 0) {
|
||||||
|
Object.defineProperty(destination.prototype, kMixinToJSON, {
|
||||||
|
writable: true,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
// eslint-disable-next-line func-name-matching
|
||||||
|
value: function _mixinToJSON(data: Partial<unknown>) {
|
||||||
|
for (const enricher of enrichToJSONs) {
|
||||||
|
enricher.call(this, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the properties (setters) of each mixins template to the destinations template
|
||||||
|
if (dataTemplates.length > 0) {
|
||||||
|
if (!Object.getOwnPropertyDescriptor(destination, DataTemplatePropertyName)) {
|
||||||
|
Object.defineProperty(destination, DataTemplatePropertyName, {
|
||||||
|
value: Object.defineProperties({}, Object.getOwnPropertyDescriptors(destination[DataTemplatePropertyName])),
|
||||||
|
writable: true,
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const template of dataTemplates) {
|
||||||
|
Object.defineProperties(destination[DataTemplatePropertyName], Object.getOwnPropertyDescriptors(template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/structures/src/MixinTypes.d.ts
vendored
Normal file
23
packages/structures/src/MixinTypes.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { MixinBase } from './Mixin.js';
|
||||||
|
import type { Structure } from './Structure.js';
|
||||||
|
import type { kData, kMixinConstruct } from './utils/symbols.js';
|
||||||
|
import type { CollapseUnion, MergePrototypes } from './utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type utility to provide accurate types for the runtime effects of {@link Mixin}
|
||||||
|
*
|
||||||
|
* @typeParam BaseClass - The class that is being directly extended, must match the class that the mixins are expecting
|
||||||
|
* @typeParam Mixins - The mixins that will be applied to this class via a {@link Mixin} call
|
||||||
|
*/
|
||||||
|
export type MixinTypes<BaseClass extends Structure<{}>, Mixins extends readonly MixinBase<BaseClass>[]> = CollapseUnion<
|
||||||
|
BaseClass extends Structure<infer DataType, infer Omitted>
|
||||||
|
? Mixins[number] extends Structure<DataType, Omitted>
|
||||||
|
? // prettier-ignore
|
||||||
|
Structure<DataType, Omitted>[typeof kData] extends
|
||||||
|
// @ts-expect-error kData is protected
|
||||||
|
Mixins[number][typeof kData]
|
||||||
|
? Omit<MergePrototypes<Mixins>, keyof BaseClass | typeof kMixinConstruct>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
>;
|
||||||
144
packages/structures/src/Structure.ts
Normal file
144
packages/structures/src/Structure.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { kClone, kData, kMixinConstruct, kMixinToJSON, kPatch } from './utils/symbols.js';
|
||||||
|
import type { ReplaceOmittedWithUnknown } from './utils/types.js';
|
||||||
|
|
||||||
|
export const DataTemplatePropertyName = 'DataTemplate';
|
||||||
|
export const OptimizeDataPropertyName = 'optimizeData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a data model from the Discord API
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* Explanation of the type complexity surround Structure:
|
||||||
|
*
|
||||||
|
* There are two layers of Omitted generics, one here, which allows omitting things at the library level so we do not accidentally
|
||||||
|
* access them, in addition to whatever the user does at the layer above.
|
||||||
|
*
|
||||||
|
* The second layer, in the exported structure is effectively a type cast that allows the getters types to match whatever data template is used
|
||||||
|
*
|
||||||
|
* In order to safely set and access this data, the constructor and patch take data as "partial" and forcibly assigns it to kData. To accommodate this,
|
||||||
|
* kData stores properties as `unknown` when it is omitted, which allows accessing the property in getters even when it may not actually be present.
|
||||||
|
* This is the most technically correct way of representing the value, especially since there is no way to guarantee runtime matches the "type cast."
|
||||||
|
*/
|
||||||
|
export abstract class Structure<DataType extends {}, Omitted extends keyof DataType | '' = ''> {
|
||||||
|
/**
|
||||||
|
* A construct function used when mixing to allow mixins to set optimized property defaults
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @remarks This should only be used to set defaults, setting optimized values should be done
|
||||||
|
* in the mixins `optimizeData` method, which will be called automatically.
|
||||||
|
* @param data - The full API data received by the Structure
|
||||||
|
*/
|
||||||
|
protected [kMixinConstruct]?(data: Partial<DataType>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function used when mixing to allow mixins to add properties to the result of toJSON
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @remarks This should only be used to add properties that the mixin optimizes, if the raw
|
||||||
|
* JSON data is unchanged the property will already be returned.
|
||||||
|
* @param data - The result of the base class toJSON Structure before it gets returned
|
||||||
|
*/
|
||||||
|
protected [kMixinToJSON]?(data: Partial<DataType>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each Structure.
|
||||||
|
*
|
||||||
|
* @remarks This template should be overridden in all subclasses to provide more accurate type information.
|
||||||
|
* The template in the base {@link Structure} class will have no effect on most subclasses for this reason.
|
||||||
|
*/
|
||||||
|
protected static readonly DataTemplate: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns A cloned version of the data template, ready to create a new data object.
|
||||||
|
*/
|
||||||
|
private getDataTemplate() {
|
||||||
|
return Object.create((this.constructor as typeof Structure).DataTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw data from the API for this structure
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected [kData]: Readonly<ReplaceOmittedWithUnknown<Omitted, DataType>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new structure to represent API data
|
||||||
|
*
|
||||||
|
* @param data - the data from the API that this structure will represent
|
||||||
|
* @remarks To be made public in subclasses
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public constructor(data: Readonly<Partial<DataType>>, ..._rest: unknown[]) {
|
||||||
|
this[kData] = Object.assign(this.getDataTemplate(), data);
|
||||||
|
this[kMixinConstruct]?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patches the raw data of this object in place
|
||||||
|
*
|
||||||
|
* @param data - the updated data from the API to patch with
|
||||||
|
* @remarks To be made public in subclasses
|
||||||
|
* @returns this
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected [kPatch](data: Readonly<Partial<DataType>>): this {
|
||||||
|
this[kData] = Object.assign(this.getDataTemplate(), this[kData], data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a clone of this structure
|
||||||
|
*
|
||||||
|
* @returns a clone of this
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected [kClone](patchPayload?: Readonly<Partial<DataType>>): typeof this {
|
||||||
|
const clone = this.toJSON();
|
||||||
|
// @ts-expect-error constructor is of abstract class is unknown
|
||||||
|
return new this.constructor(
|
||||||
|
// Ensure the ts-expect-error only applies to the constructor call
|
||||||
|
patchPayload ? Object.assign(clone, patchPayload) : clone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called to ensure stored raw data is in optimized formats, used in tandem with a data template
|
||||||
|
*
|
||||||
|
* @example created_timestamp is an ISO string, this can be stored in optimized form as a number
|
||||||
|
* @param _data - the raw data received from the API to optimize
|
||||||
|
* @remarks Implementation to be done in subclasses and mixins where needed.
|
||||||
|
* For typescript users, mixins must use the closest ancestors access modifier.
|
||||||
|
* @remarks Automatically called in Structure[kPatch] but must be called manually in the constructor
|
||||||
|
* of any class implementing this method.
|
||||||
|
* @remarks Additionally, when implementing, ensure to call `super._optimizeData` if any class in the super chain aside
|
||||||
|
* from Structure contains an implementation.
|
||||||
|
* Note: mixins do not need to call super ever as the process of mixing walks the prototype chain.
|
||||||
|
* @virtual
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected optimizeData(_data: Partial<DataType>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this object to its JSON format with raw API data (or close to it),
|
||||||
|
* automatically called by `JSON.stringify()` when this structure is stringified
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The type of this data is determined by omissions at runtime and is only guaranteed for default omissions
|
||||||
|
* @privateRemarks
|
||||||
|
* When omitting properties at the library level, this must be overridden to re-add those properties
|
||||||
|
*/
|
||||||
|
public toJSON(): DataType {
|
||||||
|
// This will be DataType provided nothing is omitted, when omits occur, subclass needs to overwrite this.
|
||||||
|
const data =
|
||||||
|
// Spread is way faster than structuredClone, but is shallow. So use it only if there is no nested objects
|
||||||
|
(
|
||||||
|
Object.values(this[kData]).some((value) => typeof value === 'object' && value !== null)
|
||||||
|
? structuredClone(this[kData])
|
||||||
|
: { ...this[kData] }
|
||||||
|
) as DataType;
|
||||||
|
this[kMixinToJSON]?.(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
packages/structures/src/bitfields/BitField.ts
Normal file
203
packages/structures/src/bitfields/BitField.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import type { EnumLike, NonAbstract, RecursiveReadonlyArray } from '../utils/types.js';
|
||||||
|
|
||||||
|
// TODO: this currently is mostly copied from mainlib discord.js v14 and definitely needs a refactor in a later iteration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data that can be resolved to give a bit field. This can be:
|
||||||
|
* A bit number (this can be a number literal or a value taken from {@link (BitField:class).Flags})
|
||||||
|
* A string bit number
|
||||||
|
* An instance of BitField
|
||||||
|
* An Array of BitFieldResolvable
|
||||||
|
*/
|
||||||
|
export type BitFieldResolvable<Flags extends string> =
|
||||||
|
| Flags
|
||||||
|
| Readonly<BitField<Flags>>
|
||||||
|
| RecursiveReadonlyArray<Flags | Readonly<BitField<Flags>> | bigint | number | `${bigint}`>
|
||||||
|
| bigint
|
||||||
|
| number
|
||||||
|
| `${bigint}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data structure that makes it easy to interact with a bit field.
|
||||||
|
*/
|
||||||
|
export abstract class BitField<Flags extends string> {
|
||||||
|
/**
|
||||||
|
* Numeric bit field flags.
|
||||||
|
*
|
||||||
|
* @remarks Defined in extension classes
|
||||||
|
*/
|
||||||
|
public static readonly Flags: EnumLike<unknown, bigint | number> = {};
|
||||||
|
|
||||||
|
public static readonly DefaultBit: bigint = 0n;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitfield of the packed bits
|
||||||
|
*/
|
||||||
|
public bitField: bigint;
|
||||||
|
|
||||||
|
declare public ['constructor']: NonAbstract<typeof BitField<Flags>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bits - Bit(s) to read from
|
||||||
|
*/
|
||||||
|
public constructor(bits: BitFieldResolvable<Flags> = this.constructor.DefaultBit) {
|
||||||
|
this.bitField = this.constructor.resolve(bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the bit field has a bit, or any of multiple bits.
|
||||||
|
*
|
||||||
|
* @param bit - Bit(s) to check for
|
||||||
|
* @returns Whether the bit field has the bit(s)
|
||||||
|
*/
|
||||||
|
public any(bit: BitFieldResolvable<Flags>) {
|
||||||
|
return (this.bitField & this.constructor.resolve(bit)) !== this.constructor.DefaultBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this bit field equals another
|
||||||
|
*
|
||||||
|
* @param bit - Bit(s) to check for
|
||||||
|
* @returns Whether this bit field equals the other
|
||||||
|
*/
|
||||||
|
public equals(bit: BitFieldResolvable<Flags>) {
|
||||||
|
return this.bitField === this.constructor.resolve(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the bit field has a bit, or multiple bits.
|
||||||
|
*
|
||||||
|
* @param bit - Bit(s) to check for
|
||||||
|
* @returns Whether the bit field has the bit(s)
|
||||||
|
*/
|
||||||
|
public has(bit: BitFieldResolvable<Flags>, ..._hasParams: unknown[]) {
|
||||||
|
const resolvedBit = this.constructor.resolve(bit);
|
||||||
|
return (this.bitField & resolvedBit) === resolvedBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all given bits that are missing from the bit field.
|
||||||
|
*
|
||||||
|
* @param bits - Bit(s) to check for
|
||||||
|
* @param hasParams - Additional parameters for the has method, if any
|
||||||
|
* @returns A bit field containing the missing bits
|
||||||
|
*/
|
||||||
|
public missing(bits: BitFieldResolvable<Flags>, ...hasParams: readonly unknown[]) {
|
||||||
|
return new this.constructor(bits).remove(this).toArray(...hasParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freezes these bits, making them immutable.
|
||||||
|
*
|
||||||
|
* @returns This bit field but frozen
|
||||||
|
*/
|
||||||
|
public freeze() {
|
||||||
|
return Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds bits to these ones.
|
||||||
|
*
|
||||||
|
* @param bits - Bits to add
|
||||||
|
* @returns These bits or new BitField if the instance is frozen.
|
||||||
|
*/
|
||||||
|
public add(...bits: BitFieldResolvable<Flags>[]) {
|
||||||
|
let total = this.constructor.DefaultBit;
|
||||||
|
for (const bit of bits) {
|
||||||
|
total |= this.constructor.resolve(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.isFrozen(this)) return new this.constructor(this.bitField | total);
|
||||||
|
this.bitField |= total;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes bits from these.
|
||||||
|
*
|
||||||
|
* @param bits - Bits to remove
|
||||||
|
* @returns These bits or new BitField if the instance is frozen.
|
||||||
|
*/
|
||||||
|
public remove(...bits: BitFieldResolvable<Flags>[]) {
|
||||||
|
let total = this.constructor.DefaultBit;
|
||||||
|
for (const bit of bits) {
|
||||||
|
total |= this.constructor.resolve(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.isFrozen(this)) return new this.constructor(this.bitField & ~total);
|
||||||
|
this.bitField &= ~total;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an object mapping field names to a boolean indicating whether the bit is available.
|
||||||
|
*
|
||||||
|
* @param hasParams - Additional parameters for the has method, if any
|
||||||
|
* @returns An object mapping field names to a boolean indicating whether the bit is available
|
||||||
|
*/
|
||||||
|
public serialize(...hasParams: readonly unknown[]) {
|
||||||
|
const serialized: Partial<Record<keyof Flags, boolean>> = {};
|
||||||
|
for (const [flag, bit] of Object.entries(this.constructor.Flags)) {
|
||||||
|
if (Number.isNaN(Number(flag))) serialized[flag as keyof Flags] = this.has(bit as bigint | number, ...hasParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an Array of bit field names based on the bits available.
|
||||||
|
*
|
||||||
|
* @param hasParams - Additional parameters for the has method, if any
|
||||||
|
* @returns An Array of bit field names
|
||||||
|
*/
|
||||||
|
public toArray(...hasParams: readonly unknown[]) {
|
||||||
|
return [...this[Symbol.iterator](...hasParams)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON(asNumber?: boolean) {
|
||||||
|
if (asNumber) {
|
||||||
|
if (this.bitField > Number.MAX_SAFE_INTEGER) {
|
||||||
|
throw new RangeError(
|
||||||
|
`Cannot convert bitfield value ${this.bitField} to number, as it is bigger than ${Number.MAX_SAFE_INTEGER} (the maximum safe integer)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(this.bitField);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.bitField.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public valueOf() {
|
||||||
|
return this.bitField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public *[Symbol.iterator](...hasParams: unknown[]) {
|
||||||
|
for (const bitName of Object.keys(this.constructor.Flags)) {
|
||||||
|
if (Number.isNaN(Number(bitName)) && this.has(bitName as Flags, ...hasParams)) yield bitName as Flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves bit fields to their numeric form.
|
||||||
|
*
|
||||||
|
* @param bit - bit(s) to resolve
|
||||||
|
* @returns the numeric value of the bit fields
|
||||||
|
*/
|
||||||
|
public static resolve<Flags extends string = string>(bit: BitFieldResolvable<Flags>): bigint {
|
||||||
|
const DefaultBit = this.DefaultBit;
|
||||||
|
if (typeof bit === 'bigint' && bit >= DefaultBit) return bit;
|
||||||
|
if (typeof bit === 'number' && BigInt(bit) >= DefaultBit) return BigInt(bit);
|
||||||
|
if (bit instanceof BitField) return bit.bitField;
|
||||||
|
if (Array.isArray(bit)) {
|
||||||
|
return bit.map((bit_) => this.resolve(bit_)).reduce((prev, bit_) => prev | bit_, DefaultBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof bit === 'string') {
|
||||||
|
if (!Number.isNaN(Number(bit))) return BigInt(bit);
|
||||||
|
if (bit in this.Flags) return this.Flags[bit as keyof typeof this.Flags];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`BitFieldInvalid: ${JSON.stringify(bit)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/structures/src/bitfields/ChannelFlagsBitField.ts
Normal file
16
packages/structures/src/bitfields/ChannelFlagsBitField.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ChannelFlags } from 'discord-api-types/v10';
|
||||||
|
import { BitField } from './BitField.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data structure that makes it easy to interact with a {@link (Channel:class).flags} bitfield.
|
||||||
|
*/
|
||||||
|
export class ChannelFlagsBitField extends BitField<keyof ChannelFlags> {
|
||||||
|
/**
|
||||||
|
* Numeric guild channel flags.
|
||||||
|
*/
|
||||||
|
public static override readonly Flags = ChannelFlags;
|
||||||
|
|
||||||
|
public override toJSON() {
|
||||||
|
return super.toJSON(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
packages/structures/src/bitfields/PermissionsBitField.ts
Normal file
76
packages/structures/src/bitfields/PermissionsBitField.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* eslint-disable unicorn/consistent-function-scoping */
|
||||||
|
import { PermissionFlagsBits } from 'discord-api-types/v10';
|
||||||
|
import type { BitFieldResolvable } from './BitField.js';
|
||||||
|
import { BitField } from './BitField.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data structure that makes it easy to interact with a permission bit field. All {@link GuildMember}s have a set of
|
||||||
|
* permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrite}s for the member
|
||||||
|
* that override their default permissions.
|
||||||
|
*/
|
||||||
|
export class PermissionsBitField extends BitField<keyof typeof PermissionFlagsBits> {
|
||||||
|
/**
|
||||||
|
* Numeric permission flags.
|
||||||
|
*
|
||||||
|
* @see {@link https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags}
|
||||||
|
*/
|
||||||
|
public static override Flags = PermissionFlagsBits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bit field representing every permission combined
|
||||||
|
*/
|
||||||
|
public static readonly All = Object.values(PermissionFlagsBits).reduce((all, perm) => all | perm, 0n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bit field representing the default permissions for users
|
||||||
|
*/
|
||||||
|
public static readonly Default = 104_324_673n;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bit field representing the permissions required for moderators of stage channels
|
||||||
|
*/
|
||||||
|
public static readonly StageModerator =
|
||||||
|
PermissionFlagsBits.ManageChannels | PermissionFlagsBits.MuteMembers | PermissionFlagsBits.MoveMembers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all given bits that are missing from the bit field.
|
||||||
|
*
|
||||||
|
* @param bits - Bit(s) to check for
|
||||||
|
* @param checkAdmin - Whether to allow the administrator permission to override
|
||||||
|
* @returns A bit field containing the missing permissions
|
||||||
|
*/
|
||||||
|
public override missing(bits: BitFieldResolvable<keyof typeof PermissionFlagsBits>, checkAdmin = true) {
|
||||||
|
return checkAdmin && this.has(PermissionFlagsBits.Administrator) ? [] : super.missing(bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the bit field has a permission, or any of multiple permissions.
|
||||||
|
*
|
||||||
|
* @param permission - Permission(s) to check for
|
||||||
|
* @param checkAdmin - Whether to allow the administrator permission to override
|
||||||
|
* @returns Whether the bit field has the permission(s)
|
||||||
|
*/
|
||||||
|
public override any(permission: BitFieldResolvable<keyof typeof PermissionFlagsBits>, checkAdmin = true) {
|
||||||
|
return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.any(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the bit field has a permission, or multiple permissions.
|
||||||
|
*
|
||||||
|
* @param permission - Permission(s) to check for
|
||||||
|
* @param checkAdmin - Whether to allow the administrator permission to override
|
||||||
|
* @returns Whether the bit field has the permission(s)
|
||||||
|
*/
|
||||||
|
public override has(permission: BitFieldResolvable<keyof typeof PermissionFlagsBits>, checkAdmin = true) {
|
||||||
|
return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.has(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an Array of bitfield names based on the permissions available.
|
||||||
|
*
|
||||||
|
* @returns An Array of permission names
|
||||||
|
*/
|
||||||
|
public override toArray() {
|
||||||
|
return super.toArray(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/structures/src/bitfields/index.ts
Normal file
4
packages/structures/src/bitfields/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './BitField.js';
|
||||||
|
|
||||||
|
export * from './ChannelFlagsBitField.js';
|
||||||
|
export * from './PermissionsBitField.js';
|
||||||
46
packages/structures/src/channels/AnnouncementChannel.ts
Normal file
46
packages/structures/src/channels/AnnouncementChannel.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { APINewsChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
|
||||||
|
export interface AnnouncementChannel<Omitted extends keyof APINewsChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildAnnouncement>,
|
||||||
|
[
|
||||||
|
TextChannelMixin<ChannelType.GuildAnnouncement>,
|
||||||
|
ChannelParentMixin<ChannelType.GuildAnnouncement>,
|
||||||
|
ChannelPermissionMixin<ChannelType.GuildAnnouncement>,
|
||||||
|
ChannelPinMixin<ChannelType.GuildAnnouncement>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.GuildAnnouncement>,
|
||||||
|
ChannelTopicMixin<ChannelType.GuildAnnouncement>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for announcement channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class AnnouncementChannel<Omitted extends keyof APINewsChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildAnnouncement,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APINewsChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(AnnouncementChannel, [
|
||||||
|
TextChannelMixin,
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPermissionMixin,
|
||||||
|
ChannelPinMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
ChannelTopicMixin,
|
||||||
|
]);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { APIAnnouncementThreadChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
|
||||||
|
|
||||||
|
export interface AnnouncementThreadChannel<Omitted extends keyof APIAnnouncementThreadChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.AnnouncementThread>,
|
||||||
|
[
|
||||||
|
TextChannelMixin<ChannelType.AnnouncementThread>,
|
||||||
|
ChannelOwnerMixin<ChannelType.AnnouncementThread>,
|
||||||
|
ChannelParentMixin<ChannelType.AnnouncementThread>,
|
||||||
|
ChannelPinMixin<ChannelType.AnnouncementThread>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.AnnouncementThread>,
|
||||||
|
GuildChannelMixin<ChannelType.AnnouncementThread>,
|
||||||
|
ThreadChannelMixin<ChannelType.AnnouncementThread>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for announcement threads, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class AnnouncementThreadChannel<Omitted extends keyof APIAnnouncementThreadChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.AnnouncementThread,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIAnnouncementThreadChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData?.(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(AnnouncementThreadChannel, [
|
||||||
|
TextChannelMixin,
|
||||||
|
ChannelOwnerMixin,
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPinMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
GuildChannelMixin,
|
||||||
|
ThreadChannelMixin,
|
||||||
|
]);
|
||||||
28
packages/structures/src/channels/CategoryChannel.ts
Normal file
28
packages/structures/src/channels/CategoryChannel.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { APIGuildCategoryChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
|
||||||
|
|
||||||
|
export interface CategoryChannel<Omitted extends keyof APIGuildCategoryChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildCategory>,
|
||||||
|
[ChannelPermissionMixin<ChannelType.GuildCategory>, GuildChannelMixin<ChannelType.GuildCategory>]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for category channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class CategoryChannel<Omitted extends keyof APIGuildCategoryChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildCategory,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIGuildCategoryChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(CategoryChannel, [ChannelPermissionMixin, GuildChannelMixin]);
|
||||||
185
packages/structures/src/channels/Channel.ts
Normal file
185
packages/structures/src/channels/Channel.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||||
|
import type { APIChannel, APIPartialChannel, ChannelType, ChannelFlags } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { ChannelFlagsBitField } from '../bitfields/ChannelFlagsBitField.js';
|
||||||
|
import { kData, kPatch } from '../utils/symbols.js';
|
||||||
|
import { isIdSet } from '../utils/type-guards.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import type { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import type { ChannelWebhookMixin } from './mixins/ChannelWebhookMixin.js';
|
||||||
|
import type { DMChannelMixin } from './mixins/DMChannelMixin.js';
|
||||||
|
import type { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
|
||||||
|
import type { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
import type { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
|
||||||
|
import type { ThreadOnlyChannelMixin } from './mixins/ThreadOnlyChannelMixin.js';
|
||||||
|
import type { VoiceChannelMixin } from './mixins/VoiceChannelMixin.js';
|
||||||
|
|
||||||
|
export type PartialChannel = Channel<ChannelType, Exclude<keyof APIChannel, keyof APIPartialChannel>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data stored by a {@link Channel} structure based on its {@link (Channel:class)."type"} property.
|
||||||
|
*/
|
||||||
|
export type ChannelDataType<Type extends ChannelType | 'unknown'> = Type extends ChannelType
|
||||||
|
? Extract<APIChannel, { type: Type }>
|
||||||
|
: APIPartialChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents any channel on Discord.
|
||||||
|
*
|
||||||
|
* @typeParam Type - Specify the type of the channel being constructed for more accurate data types
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
* @remarks Although this class _can_ be instantiated directly for any channel type,
|
||||||
|
* it's intended to be subclassed with the appropriate mixins for each channel type.
|
||||||
|
*/
|
||||||
|
export class Channel<
|
||||||
|
Type extends ChannelType | 'unknown' = ChannelType,
|
||||||
|
Omitted extends keyof ChannelDataType<Type> | '' = '',
|
||||||
|
> extends Structure<ChannelDataType<Type>, Omitted> {
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each Channel.
|
||||||
|
*
|
||||||
|
* @remarks This template is only guaranteed to apply to channels constructed directly via `new Channel()`.
|
||||||
|
* Use the appropriate subclass template to remove data from that channel type.
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIChannel> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data - The raw data received from the API for the channel
|
||||||
|
*/
|
||||||
|
public constructor(data: Partialize<ChannelDataType<Type>, Omitted>) {
|
||||||
|
super(data as ChannelDataType<Type>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.[kPatch]}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public override [kPatch](data: Partial<ChannelDataType<Type>>) {
|
||||||
|
return super[kPatch](data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the channel
|
||||||
|
*/
|
||||||
|
public get id() {
|
||||||
|
return this[kData].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the channel
|
||||||
|
*/
|
||||||
|
public get type() {
|
||||||
|
// This cast can be incorrect when type is omitted and if the wrong type of channel was constructed
|
||||||
|
return this[kData].type as Type extends 'unknown' ? number : Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the channel, null for DMs
|
||||||
|
*
|
||||||
|
* @privateRemarks The type of `name` can be narrowed in Guild Channels and DM channels to string and null respectively,
|
||||||
|
* respecting Omit behaviors
|
||||||
|
*/
|
||||||
|
public get name() {
|
||||||
|
return this[kData].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flags that are applied to the channel.
|
||||||
|
*
|
||||||
|
* @privateRemarks The type of `flags` can be narrowed in Guild Channels and DMChannel to ChannelFlags, and in GroupDM channel
|
||||||
|
* to null, respecting Omit behaviors
|
||||||
|
*/
|
||||||
|
public get flags() {
|
||||||
|
const flags =
|
||||||
|
'flags' in this[kData] && typeof this[kData].flags === 'number' ? (this[kData].flags as ChannelFlags) : null;
|
||||||
|
return flags ? new ChannelFlagsBitField(flags) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp the channel was created at
|
||||||
|
*/
|
||||||
|
public get createdTimestamp() {
|
||||||
|
return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the channel was created at
|
||||||
|
*/
|
||||||
|
public get createdAt() {
|
||||||
|
const createdTimestamp = this.createdTimestamp;
|
||||||
|
return createdTimestamp ? new Date(createdTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel is a thread channel
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `ThreadChannelMixin`
|
||||||
|
*/
|
||||||
|
public isThread(): this is ThreadChannelMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel can contain messages
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `TextChannelMixin`
|
||||||
|
*/
|
||||||
|
public isTextBased(): this is TextChannelMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel is in a guild
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `GuildChannelMixin`
|
||||||
|
*/
|
||||||
|
public isGuildBased(): this is GuildChannelMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel is a DM or DM Group
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `DMChannelMixin`
|
||||||
|
*/
|
||||||
|
public isDMBased(): this is DMChannelMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel has voice connection capabilities
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `VoiceChannelMixin`
|
||||||
|
*/
|
||||||
|
public isVoiceBased(): this is VoiceChannelMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel only allows thread creation
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `ThreadOnlyChannelMixin`
|
||||||
|
*/
|
||||||
|
public isThreadOnly(): this is ThreadOnlyChannelMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel can have permission overwrites
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `ChannelPermissionsMixin`
|
||||||
|
*/
|
||||||
|
public isPermissionCapable(): this is ChannelPermissionMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel can have webhooks
|
||||||
|
*
|
||||||
|
* @privateRemarks Overridden to `true` on `ChannelWebhooksMixin`
|
||||||
|
*/
|
||||||
|
public isWebhookCapable(): this is ChannelWebhookMixin & this {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/structures/src/channels/DMChannel.ts
Normal file
26
packages/structures/src/channels/DMChannel.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { APIDMChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
|
||||||
|
import { DMChannelMixin } from './mixins/DMChannelMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
|
||||||
|
export interface DMChannel<Omitted extends keyof APIDMChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.DM>,
|
||||||
|
[DMChannelMixin<ChannelType.DM>, TextChannelMixin<ChannelType.DM>, ChannelPinMixin<ChannelType.DM>]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for dm channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class DMChannel<Omitted extends keyof APIDMChannel | '' = ''> extends Channel<ChannelType.DM, Omitted> {
|
||||||
|
public constructor(data: Partialize<APIDMChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(DMChannel, [DMChannelMixin, TextChannelMixin, ChannelPinMixin]);
|
||||||
44
packages/structures/src/channels/ForumChannel.ts
Normal file
44
packages/structures/src/channels/ForumChannel.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { APIGuildForumChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import { kData } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
|
||||||
|
import { ThreadOnlyChannelMixin } from './mixins/ThreadOnlyChannelMixin.js';
|
||||||
|
|
||||||
|
export interface ForumChannel<Omitted extends keyof APIGuildForumChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildForum>,
|
||||||
|
[
|
||||||
|
ChannelParentMixin<ChannelType.GuildForum>,
|
||||||
|
ChannelPermissionMixin<ChannelType.GuildForum>,
|
||||||
|
ChannelTopicMixin<ChannelType.GuildForum>,
|
||||||
|
ThreadOnlyChannelMixin<ChannelType.GuildForum>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for forum channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class ForumChannel<Omitted extends keyof APIGuildForumChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildForum,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIGuildForumChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default forum layout view used to display posts in this channel.
|
||||||
|
* Defaults to 0, which indicates a layout view has not been set by a channel admin.
|
||||||
|
*/
|
||||||
|
public get defaultForumLayout() {
|
||||||
|
return this[kData].default_forum_layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(ForumChannel, [ChannelParentMixin, ChannelPermissionMixin, ChannelTopicMixin, ThreadOnlyChannelMixin]);
|
||||||
57
packages/structures/src/channels/ForumTag.ts
Normal file
57
packages/structures/src/channels/ForumTag.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { APIGuildForumTag } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { kData } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents metadata of a thread channel on Discord.
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
*/
|
||||||
|
export class ForumTag<Omitted extends keyof APIGuildForumTag | '' = ''> extends Structure<APIGuildForumTag, Omitted> {
|
||||||
|
public constructor(data: Partialize<APIGuildForumTag, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the tag.
|
||||||
|
*/
|
||||||
|
public get id() {
|
||||||
|
return this[kData].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the tag.
|
||||||
|
*/
|
||||||
|
public get name() {
|
||||||
|
return this[kData].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this tag can only be added to or removed from threads by a member with the {@link discord-api-types/v10#(PermissionFlagsBits:variable) | ManageThreads} permission.
|
||||||
|
*/
|
||||||
|
public get moderated() {
|
||||||
|
return this[kData].moderated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of a guild's custom emoji.
|
||||||
|
*/
|
||||||
|
public get emojiId() {
|
||||||
|
return this[kData].emoji_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unicode character of the emoji.
|
||||||
|
*/
|
||||||
|
public get emojiName() {
|
||||||
|
return this[kData].emoji_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The textual representation of this tag's emoji. Either a unicode character or a guild emoji mention.
|
||||||
|
*/
|
||||||
|
public get emoji() {
|
||||||
|
return this.emojiName ?? `<:_:${this.emojiId}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/structures/src/channels/GroupDMChannel.ts
Normal file
35
packages/structures/src/channels/GroupDMChannel.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { APIGroupDMChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
|
||||||
|
import { DMChannelMixin } from './mixins/DMChannelMixin.js';
|
||||||
|
import { GroupDMMixin } from './mixins/GroupDMMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
|
||||||
|
export interface GroupDMChannel<Omitted extends keyof APIGroupDMChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GroupDM>,
|
||||||
|
[
|
||||||
|
DMChannelMixin<ChannelType.GroupDM>,
|
||||||
|
TextChannelMixin<ChannelType.GroupDM>,
|
||||||
|
ChannelOwnerMixin<ChannelType.GroupDM>,
|
||||||
|
GroupDMMixin,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for group dm channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class GroupDMChannel<Omitted extends keyof APIGroupDMChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GroupDM,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIGroupDMChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(GroupDMChannel, [DMChannelMixin, TextChannelMixin, ChannelOwnerMixin, GroupDMMixin]);
|
||||||
35
packages/structures/src/channels/MediaChannel.ts
Normal file
35
packages/structures/src/channels/MediaChannel.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { APIGuildMediaChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
|
||||||
|
import { ThreadOnlyChannelMixin } from './mixins/ThreadOnlyChannelMixin.js';
|
||||||
|
|
||||||
|
export interface MediaChannel<Omitted extends keyof APIGuildMediaChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildMedia>,
|
||||||
|
[
|
||||||
|
ChannelParentMixin<ChannelType.GuildMedia>,
|
||||||
|
ChannelPermissionMixin<ChannelType.GuildMedia>,
|
||||||
|
ChannelTopicMixin<ChannelType.GuildMedia>,
|
||||||
|
ThreadOnlyChannelMixin<ChannelType.GuildMedia>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for media channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class MediaChannel<Omitted extends keyof APIGuildMediaChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildMedia,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIGuildMediaChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(MediaChannel, [ChannelParentMixin, ChannelPermissionMixin, ChannelTopicMixin, ThreadOnlyChannelMixin]);
|
||||||
94
packages/structures/src/channels/PermissionOverwrite.ts
Normal file
94
packages/structures/src/channels/PermissionOverwrite.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import type { APIOverwrite } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { PermissionsBitField } from '../bitfields/PermissionsBitField.js';
|
||||||
|
import { kAllow, kData, kDeny } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents metadata of a thread channel on Discord.
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
*/
|
||||||
|
export class PermissionOverwrite<Omitted extends keyof APIOverwrite | '' = 'allow' | 'deny'> extends Structure<
|
||||||
|
APIOverwrite,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
protected [kAllow]: bigint | null = null;
|
||||||
|
|
||||||
|
protected [kDeny]: bigint | null = null;
|
||||||
|
|
||||||
|
public constructor(data: Partialize<APIOverwrite, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each ThreadMetadata
|
||||||
|
*
|
||||||
|
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
|
||||||
|
* use `Object.defineProperties`. To override the defaults, set this value directly.
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIOverwrite> = {
|
||||||
|
set allow(_: string) {},
|
||||||
|
set deny(_: string) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.optimizeData}
|
||||||
|
*/
|
||||||
|
protected override optimizeData(data: Partial<APIOverwrite>) {
|
||||||
|
if (data.allow) {
|
||||||
|
this[kAllow] = BigInt(data.allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.deny) {
|
||||||
|
this[kDeny] = BigInt(data.deny);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission bit set allowed by this overwrite.
|
||||||
|
*/
|
||||||
|
public get allow() {
|
||||||
|
const allow = this[kAllow];
|
||||||
|
return typeof allow === 'bigint' ? new PermissionsBitField(allow) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission bit set denied by this overwrite.
|
||||||
|
*/
|
||||||
|
public get deny() {
|
||||||
|
const deny = this[kDeny];
|
||||||
|
return typeof deny === 'bigint' ? new PermissionsBitField(deny) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The role or user id for this overwrite.
|
||||||
|
*/
|
||||||
|
public get id() {
|
||||||
|
return this[kData].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this overwrite.
|
||||||
|
*/
|
||||||
|
public get type() {
|
||||||
|
return this[kData].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON() {
|
||||||
|
const clone = super.toJSON();
|
||||||
|
if (this[kAllow]) {
|
||||||
|
clone.allow = this[kAllow].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this[kDeny]) {
|
||||||
|
clone.deny = this[kDeny].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
packages/structures/src/channels/PrivateThreadChannel.ts
Normal file
46
packages/structures/src/channels/PrivateThreadChannel.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { APIPrivateThreadChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
|
||||||
|
|
||||||
|
export interface PrivateThreadChannel<Omitted extends keyof APIPrivateThreadChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.PrivateThread>,
|
||||||
|
[
|
||||||
|
TextChannelMixin<ChannelType.PrivateThread>,
|
||||||
|
ChannelOwnerMixin<ChannelType.PrivateThread>,
|
||||||
|
ChannelParentMixin<ChannelType.PrivateThread>,
|
||||||
|
ChannelPinMixin<ChannelType.PrivateThread>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.PrivateThread>,
|
||||||
|
ThreadChannelMixin<ChannelType.PrivateThread>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for private thread channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class PrivateThreadChannel<Omitted extends keyof APIPrivateThreadChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.PrivateThread,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIPrivateThreadChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(PrivateThreadChannel, [
|
||||||
|
TextChannelMixin,
|
||||||
|
ChannelOwnerMixin,
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPinMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
ThreadChannelMixin,
|
||||||
|
]);
|
||||||
49
packages/structures/src/channels/PublicThreadChannel.ts
Normal file
49
packages/structures/src/channels/PublicThreadChannel.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { APIPublicThreadChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { AppliedTagsMixin } from './mixins/AppliedTagsMixin.js';
|
||||||
|
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
|
||||||
|
|
||||||
|
export interface PublicThreadChannel<Omitted extends keyof APIPublicThreadChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.PublicThread>,
|
||||||
|
[
|
||||||
|
TextChannelMixin<ChannelType.PublicThread>,
|
||||||
|
ChannelOwnerMixin<ChannelType.PublicThread>,
|
||||||
|
ChannelParentMixin<ChannelType.PublicThread>,
|
||||||
|
ChannelPinMixin<ChannelType.PublicThread>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.PublicThread>,
|
||||||
|
ThreadChannelMixin<ChannelType.PublicThread>,
|
||||||
|
AppliedTagsMixin,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample Implementation of a structure for public thread channels, usable by direct end consumers.
|
||||||
|
*/
|
||||||
|
export class PublicThreadChannel<Omitted extends keyof APIPublicThreadChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.PublicThread,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIPublicThreadChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(PublicThreadChannel, [
|
||||||
|
TextChannelMixin,
|
||||||
|
ChannelOwnerMixin,
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPinMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
ThreadChannelMixin,
|
||||||
|
AppliedTagsMixin,
|
||||||
|
]);
|
||||||
40
packages/structures/src/channels/StageChannel.ts
Normal file
40
packages/structures/src/channels/StageChannel.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { APIGuildStageVoiceChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { ChannelWebhookMixin } from './mixins/ChannelWebhookMixin.js';
|
||||||
|
import { VoiceChannelMixin } from './mixins/VoiceChannelMixin.js';
|
||||||
|
|
||||||
|
export interface StageChannel<Omitted extends keyof APIGuildStageVoiceChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildStageVoice>,
|
||||||
|
[
|
||||||
|
ChannelParentMixin<ChannelType.GuildStageVoice>,
|
||||||
|
ChannelPermissionMixin<ChannelType.GuildStageVoice>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.GuildStageVoice>,
|
||||||
|
ChannelWebhookMixin<ChannelType.GuildStageVoice>,
|
||||||
|
VoiceChannelMixin<ChannelType.GuildStageVoice>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
export class StageChannel<Omitted extends keyof APIGuildStageVoiceChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildStageVoice,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIGuildStageVoiceChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(StageChannel, [
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPermissionMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
ChannelWebhookMixin,
|
||||||
|
VoiceChannelMixin,
|
||||||
|
]);
|
||||||
43
packages/structures/src/channels/TextChannel.ts
Normal file
43
packages/structures/src/channels/TextChannel.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { APITextChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
|
||||||
|
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
|
||||||
|
|
||||||
|
export interface TextChannel<Omitted extends keyof APITextChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildText>,
|
||||||
|
[
|
||||||
|
TextChannelMixin<ChannelType.GuildText>,
|
||||||
|
ChannelParentMixin<ChannelType.GuildText>,
|
||||||
|
ChannelPermissionMixin<ChannelType.GuildText>,
|
||||||
|
ChannelPinMixin<ChannelType.GuildText>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.GuildText>,
|
||||||
|
ChannelTopicMixin<ChannelType.GuildText>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
export class TextChannel<Omitted extends keyof APITextChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildText,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APITextChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(TextChannel, [
|
||||||
|
TextChannelMixin,
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPermissionMixin,
|
||||||
|
ChannelPinMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
ChannelTopicMixin,
|
||||||
|
]);
|
||||||
120
packages/structures/src/channels/ThreadMetadata.ts
Normal file
120
packages/structures/src/channels/ThreadMetadata.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import type { APIThreadMetadata } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { kArchiveTimestamp, kCreatedTimestamp, kData } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents metadata of a thread channel on Discord.
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
*/
|
||||||
|
export class ThreadMetadata<
|
||||||
|
Omitted extends keyof APIThreadMetadata | '' = 'archive_timestamp' | 'create_timestamp',
|
||||||
|
> extends Structure<APIThreadMetadata, Omitted> {
|
||||||
|
protected [kArchiveTimestamp]: number | null = null;
|
||||||
|
|
||||||
|
protected [kCreatedTimestamp]: number | null = null;
|
||||||
|
|
||||||
|
public constructor(data: Partialize<APIThreadMetadata, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each ThreadMetadata
|
||||||
|
*
|
||||||
|
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
|
||||||
|
* use `Object.defineProperties`. To override the defaults, set this value directly.
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIThreadMetadata> = {
|
||||||
|
set create_timestamp(_: string) {},
|
||||||
|
set archive_timestamp(_: string) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.optimizeData}
|
||||||
|
*/
|
||||||
|
protected override optimizeData(data: Partial<APIThreadMetadata>) {
|
||||||
|
if (data.create_timestamp) {
|
||||||
|
this[kCreatedTimestamp] = Date.parse(data.create_timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.archive_timestamp) {
|
||||||
|
this[kArchiveTimestamp] = Date.parse(data.archive_timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the thread is archived.
|
||||||
|
*/
|
||||||
|
public get archived() {
|
||||||
|
return this[kData].archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp when the thread's archive status was last changed, used for calculating recent activity.
|
||||||
|
*/
|
||||||
|
public get archivedTimestamp() {
|
||||||
|
return this[kArchiveTimestamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp when the thread was created; only populated for threads created after 2022-01-09.
|
||||||
|
*/
|
||||||
|
public get createdTimestamp() {
|
||||||
|
return this[kCreatedTimestamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The thread will stop showing in the channel list after auto_archive_duration minutes of inactivity,
|
||||||
|
*/
|
||||||
|
public get autoArchiveDuration() {
|
||||||
|
return this[kData].auto_archive_duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether non-moderators can add other non-moderators to a thread; only available on private threads.
|
||||||
|
*/
|
||||||
|
public get invitable() {
|
||||||
|
return this[kData].invitable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the thread is locked; when a thread is locked, only users with {@link discord-api-types/v10#(PermissionFlagsBits:variable) | ManageThreads} can unarchive it.
|
||||||
|
*/
|
||||||
|
public get locked() {
|
||||||
|
return this[kData].locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the thread was archived at
|
||||||
|
*/
|
||||||
|
public get archivedAt() {
|
||||||
|
const archivedTimestamp = this.archivedTimestamp;
|
||||||
|
return archivedTimestamp ? new Date(archivedTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the thread was created at
|
||||||
|
*/
|
||||||
|
public get createdAt() {
|
||||||
|
const createdTimestamp = this.createdTimestamp;
|
||||||
|
return createdTimestamp ? new Date(createdTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON() {
|
||||||
|
const data = super.toJSON();
|
||||||
|
if (this[kArchiveTimestamp]) {
|
||||||
|
data.archive_timestamp = new Date(this[kArchiveTimestamp]).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this[kCreatedTimestamp]) {
|
||||||
|
data.create_timestamp = new Date(this[kCreatedTimestamp]).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/structures/src/channels/VoiceChannel.ts
Normal file
40
packages/structures/src/channels/VoiceChannel.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { APIGuildVoiceChannel, ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from '../Mixin.js';
|
||||||
|
import type { MixinTypes } from '../MixinTypes.d.ts';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
|
||||||
|
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
|
||||||
|
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
|
||||||
|
import { ChannelWebhookMixin } from './mixins/ChannelWebhookMixin.js';
|
||||||
|
import { VoiceChannelMixin } from './mixins/VoiceChannelMixin.js';
|
||||||
|
|
||||||
|
export interface VoiceChannel<Omitted extends keyof APIGuildVoiceChannel | '' = ''>
|
||||||
|
extends MixinTypes<
|
||||||
|
Channel<ChannelType.GuildVoice>,
|
||||||
|
[
|
||||||
|
ChannelParentMixin<ChannelType.GuildVoice>,
|
||||||
|
ChannelPermissionMixin<ChannelType.GuildVoice>,
|
||||||
|
ChannelSlowmodeMixin<ChannelType.GuildVoice>,
|
||||||
|
ChannelWebhookMixin<ChannelType.GuildVoice>,
|
||||||
|
VoiceChannelMixin<ChannelType.GuildVoice>,
|
||||||
|
]
|
||||||
|
> {}
|
||||||
|
|
||||||
|
export class VoiceChannel<Omitted extends keyof APIGuildVoiceChannel | '' = ''> extends Channel<
|
||||||
|
ChannelType.GuildVoice,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
public constructor(data: Partialize<APIGuildVoiceChannel, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin(VoiceChannel, [
|
||||||
|
ChannelParentMixin,
|
||||||
|
ChannelPermissionMixin,
|
||||||
|
ChannelSlowmodeMixin,
|
||||||
|
ChannelWebhookMixin,
|
||||||
|
VoiceChannelMixin,
|
||||||
|
]);
|
||||||
21
packages/structures/src/channels/index.ts
Normal file
21
packages/structures/src/channels/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export * from './mixins/index.js';
|
||||||
|
|
||||||
|
export * from './ForumTag.js';
|
||||||
|
export * from './PermissionOverwrite.js';
|
||||||
|
export * from './ThreadMetadata.js';
|
||||||
|
|
||||||
|
export * from './Channel.js';
|
||||||
|
|
||||||
|
export * from './AnnouncementChannel.js';
|
||||||
|
export * from './AnnouncementThreadChannel.js';
|
||||||
|
export * from './CategoryChannel.js';
|
||||||
|
// export * from './DirectoryChannel.js';
|
||||||
|
export * from './DMChannel.js';
|
||||||
|
export * from './ForumChannel.js';
|
||||||
|
export * from './GroupDMChannel.js';
|
||||||
|
export * from './MediaChannel.js';
|
||||||
|
export * from './PrivateThreadChannel.js';
|
||||||
|
export * from './PublicThreadChannel.js';
|
||||||
|
export * from './StageChannel.js';
|
||||||
|
export * from './TextChannel.js';
|
||||||
|
export * from './VoiceChannel.js';
|
||||||
14
packages/structures/src/channels/mixins/AppliedTagsMixin.ts
Normal file
14
packages/structures/src/channels/mixins/AppliedTagsMixin.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface AppliedTagsMixin extends Channel<ChannelType.PublicThread> {}
|
||||||
|
|
||||||
|
export class AppliedTagsMixin {
|
||||||
|
/**
|
||||||
|
* The ids of the set of tags that have been applied to a thread in a {@link (ForumChannel:class)} or a {@link (MediaChannel:class)}.
|
||||||
|
*/
|
||||||
|
public get appliedTags(): readonly string[] | null {
|
||||||
|
return Array.isArray(this[kData].applied_tags) ? this[kData].applied_tags : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/structures/src/channels/mixins/ChannelOwnerMixin.ts
Normal file
14
packages/structures/src/channels/mixins/ChannelOwnerMixin.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ChannelType, ThreadChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface ChannelOwnerMixin<Type extends ChannelType.GroupDM | ThreadChannelType> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class ChannelOwnerMixin<Type extends ChannelType.GroupDM | ThreadChannelType> {
|
||||||
|
/**
|
||||||
|
* The id of the creator of the group DM or thread
|
||||||
|
*/
|
||||||
|
public get ownerId() {
|
||||||
|
return this[kData].owner_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { ChannelType, GuildChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import { GuildChannelMixin } from './GuildChannelMixin.js';
|
||||||
|
|
||||||
|
export class ChannelParentMixin<
|
||||||
|
Type extends Exclude<GuildChannelType, ChannelType.GuildCategory | ChannelType.GuildDirectory>,
|
||||||
|
> extends GuildChannelMixin<Type> {
|
||||||
|
/**
|
||||||
|
* The id of the parent category for a channel (each parent category can contain up to 50 channels) or id of the parent channel for a thread
|
||||||
|
*/
|
||||||
|
public get parentId() {
|
||||||
|
return this[kData].parent_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the channel is nsfw
|
||||||
|
*/
|
||||||
|
public get nsfw() {
|
||||||
|
return this[kData].nsfw;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ChannelType, GuildChannelType, ThreadChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface ChannelPermissionMixin<
|
||||||
|
Type extends Exclude<GuildChannelType, ChannelType.GuildDirectory | ThreadChannelType> = Exclude<
|
||||||
|
GuildChannelType,
|
||||||
|
ChannelType.GuildDirectory | ThreadChannelType
|
||||||
|
>,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @remarks has an array of sub-structures {@link PermissionOverwrite} that extending mixins should add to their DataTemplate and _optimizeData
|
||||||
|
*/
|
||||||
|
export class ChannelPermissionMixin<
|
||||||
|
Type extends Exclude<GuildChannelType, ChannelType.GuildDirectory | ThreadChannelType> = Exclude<
|
||||||
|
GuildChannelType,
|
||||||
|
ChannelType.GuildDirectory | ThreadChannelType
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The sorting position of the channel
|
||||||
|
*/
|
||||||
|
public get position() {
|
||||||
|
return this[kData].position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel can have permission overwrites
|
||||||
|
*/
|
||||||
|
public isPermissionCapable(): this is ChannelPermissionMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
packages/structures/src/channels/mixins/ChannelPinMixin.ts
Normal file
62
packages/structures/src/channels/mixins/ChannelPinMixin.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { ChannelType, ThreadChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kLastPinTimestamp, kMixinConstruct, kMixinToJSON } from '../../utils/symbols.js';
|
||||||
|
import type { Channel, ChannelDataType } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface ChannelPinMixin<
|
||||||
|
Type extends ChannelType.DM | ChannelType.GuildAnnouncement | ChannelType.GuildText | ThreadChannelType,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class ChannelPinMixin<
|
||||||
|
Type extends ChannelType.DM | ChannelType.GuildAnnouncement | ChannelType.GuildText | ThreadChannelType,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The timestamp of when the last pin in the channel happened
|
||||||
|
*/
|
||||||
|
declare protected [kLastPinTimestamp]: number | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each Channel.
|
||||||
|
*/
|
||||||
|
public static readonly DataTemplate: Partial<
|
||||||
|
ChannelDataType<ChannelType.DM | ChannelType.GuildAnnouncement | ChannelType.GuildText | ThreadChannelType>
|
||||||
|
> = {
|
||||||
|
set last_pin_timestamp(_: string) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public [kMixinConstruct]() {
|
||||||
|
this[kLastPinTimestamp] ??= null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.optimizeData}
|
||||||
|
*/
|
||||||
|
protected optimizeData(data: Partial<ChannelDataType<Type>>) {
|
||||||
|
if (data.last_pin_timestamp) {
|
||||||
|
this[kLastPinTimestamp] = Date.parse(data.last_pin_timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of when the last pin in the channel happened.
|
||||||
|
*/
|
||||||
|
public get lastPinTimestamp() {
|
||||||
|
return this[kLastPinTimestamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Date of when the last pin in the channel happened
|
||||||
|
*/
|
||||||
|
public get lastPinAt() {
|
||||||
|
const lastPinTimestamp = this.lastPinTimestamp;
|
||||||
|
return lastPinTimestamp ? new Date(lastPinTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds data from optimized properties omitted from [kData].
|
||||||
|
*
|
||||||
|
* @param data - the result of {@link (Structure:class).toJSON}
|
||||||
|
*/
|
||||||
|
protected [kMixinToJSON](data: Partial<ChannelDataType<Type>>) {
|
||||||
|
data.last_pin_timestamp = this[kLastPinTimestamp] ? new Date(this[kLastPinTimestamp]).toISOString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { GuildTextChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import { TextChannelMixin } from './TextChannelMixin.js';
|
||||||
|
|
||||||
|
export class ChannelSlowmodeMixin<Type extends GuildTextChannelType> extends TextChannelMixin<Type> {
|
||||||
|
/**
|
||||||
|
* The rate limit per user (slowmode) of this channel.
|
||||||
|
*/
|
||||||
|
public get rateLimitPerUser() {
|
||||||
|
return this[kData].rate_limit_per_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/structures/src/channels/mixins/ChannelTopicMixin.ts
Normal file
33
packages/structures/src/channels/mixins/ChannelTopicMixin.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
import { ChannelWebhookMixin } from './ChannelWebhookMixin.js';
|
||||||
|
|
||||||
|
export interface ChannelTopicMixin<
|
||||||
|
Type extends ChannelType.GuildAnnouncement | ChannelType.GuildForum | ChannelType.GuildMedia | ChannelType.GuildText,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class ChannelTopicMixin<
|
||||||
|
Type extends ChannelType.GuildAnnouncement | ChannelType.GuildForum | ChannelType.GuildMedia | ChannelType.GuildText,
|
||||||
|
> extends ChannelWebhookMixin<Type> {
|
||||||
|
/**
|
||||||
|
* The topic of this channel.
|
||||||
|
*/
|
||||||
|
public get topic() {
|
||||||
|
return this[kData].topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration after which new threads get archived by default on this channel.
|
||||||
|
*/
|
||||||
|
public get defaultAutoArchiveDuration() {
|
||||||
|
return this[kData].default_auto_archive_duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default value for rate limit per user (slowmode) on new threads in this channel.
|
||||||
|
*/
|
||||||
|
public get defaultThreadRateLimitPerUser() {
|
||||||
|
return this[kData].default_thread_rate_limit_per_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ChannelType, GuildTextChannelType, ThreadChannelType } from 'discord-api-types/v10';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface ChannelWebhookMixin<
|
||||||
|
Type extends ChannelType.GuildForum | ChannelType.GuildMedia | Exclude<GuildTextChannelType, ThreadChannelType> =
|
||||||
|
| ChannelType.GuildForum
|
||||||
|
| ChannelType.GuildMedia
|
||||||
|
| Exclude<GuildTextChannelType, ThreadChannelType>,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class ChannelWebhookMixin<
|
||||||
|
Type extends ChannelType.GuildForum | ChannelType.GuildMedia | Exclude<GuildTextChannelType, ThreadChannelType> =
|
||||||
|
| ChannelType.GuildForum
|
||||||
|
| ChannelType.GuildMedia
|
||||||
|
| Exclude<GuildTextChannelType, ThreadChannelType>,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel can have webhooks
|
||||||
|
*/
|
||||||
|
public isWebhookCapable(): this is ChannelWebhookMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/structures/src/channels/mixins/DMChannelMixin.ts
Normal file
27
packages/structures/src/channels/mixins/DMChannelMixin.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { channelLink } from '@discordjs/formatters';
|
||||||
|
import type { ChannelType } from 'discord-api-types/v10';
|
||||||
|
import type { User } from '../../users/User.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface DMChannelMixin<
|
||||||
|
Type extends ChannelType.DM | ChannelType.GroupDM = ChannelType.DM | ChannelType.GroupDM,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @remarks has recipients, an array of sub-structures {@link User} that extending mixins should add to their DataTemplate and _optimizeData
|
||||||
|
*/
|
||||||
|
export class DMChannelMixin<Type extends ChannelType.DM | ChannelType.GroupDM = ChannelType.DM | ChannelType.GroupDM> {
|
||||||
|
/**
|
||||||
|
* The URL to this channel.
|
||||||
|
*/
|
||||||
|
public get url() {
|
||||||
|
return channelLink(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel is a DM or DM Group
|
||||||
|
*/
|
||||||
|
public isDMBased(): this is DMChannelMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/structures/src/channels/mixins/GroupDMMixin.ts
Normal file
28
packages/structures/src/channels/mixins/GroupDMMixin.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface GroupDMMixin extends Channel<ChannelType.GroupDM> {}
|
||||||
|
|
||||||
|
export class GroupDMMixin {
|
||||||
|
/**
|
||||||
|
* The icon hash of the group DM.
|
||||||
|
*/
|
||||||
|
public get icon() {
|
||||||
|
return this[kData].icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the channel is managed by an application via the `gdm.join` OAuth2 scope.
|
||||||
|
*/
|
||||||
|
public get managed() {
|
||||||
|
return this[kData].managed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application id of the group DM creator if it is bot-created.
|
||||||
|
*/
|
||||||
|
public get applicationId() {
|
||||||
|
return this[kData].application_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/structures/src/channels/mixins/GuildChannelMixin.ts
Normal file
40
packages/structures/src/channels/mixins/GuildChannelMixin.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { channelLink } from '@discordjs/formatters';
|
||||||
|
import type { GuildChannelType } from 'discord-api-types/v10';
|
||||||
|
import { ChannelFlagsBitField } from '../../bitfields/ChannelFlagsBitField.js';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface GuildChannelMixin<Type extends GuildChannelType = GuildChannelType> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class GuildChannelMixin<Type extends GuildChannelType = GuildChannelType> {
|
||||||
|
/**
|
||||||
|
* The flags that are applied to the channel.
|
||||||
|
*
|
||||||
|
* @privateRemarks The type of `flags` can be narrowed in Guild Channels and DMChannel to ChannelFlags, and in GroupDM channel
|
||||||
|
* to null, respecting Omit behaviors
|
||||||
|
*/
|
||||||
|
public get flags() {
|
||||||
|
return this[kData].flags ? new ChannelFlagsBitField(this[kData].flags) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THe id of the guild this channel is in.
|
||||||
|
*/
|
||||||
|
public get guildId() {
|
||||||
|
return this[kData].guild_id!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to this channel.
|
||||||
|
*/
|
||||||
|
public get url() {
|
||||||
|
return channelLink(this.id, this.guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel is in a guild
|
||||||
|
*/
|
||||||
|
public isGuildBased(): this is GuildChannelMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/structures/src/channels/mixins/TextChannelMixin.ts
Normal file
21
packages/structures/src/channels/mixins/TextChannelMixin.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { TextChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface TextChannelMixin<Type extends TextChannelType = TextChannelType> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class TextChannelMixin<Type extends TextChannelType = TextChannelType> {
|
||||||
|
/**
|
||||||
|
* The id of the last message sent in this channel.
|
||||||
|
*/
|
||||||
|
public get lastMessageId() {
|
||||||
|
return this[kData].last_message_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel can contain messages
|
||||||
|
*/
|
||||||
|
public isTextBased(): this is TextChannelMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { ThreadChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface ThreadChannelMixin<Type extends ThreadChannelType = ThreadChannelType> extends Channel<Type> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @remarks has a sub-structure {@link ThreadMetadata} that extending mixins should add to their DataTemplate and _optimizeData
|
||||||
|
*/
|
||||||
|
export class ThreadChannelMixin<Type extends ThreadChannelType = ThreadChannelType> {
|
||||||
|
/**
|
||||||
|
* The approximate count of users in a thread, stops counting at 50
|
||||||
|
*/
|
||||||
|
public get memberCount() {
|
||||||
|
return this[kData].member_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of messages (not including the initial message or deleted messages) in a thread.
|
||||||
|
*/
|
||||||
|
public get messageCount() {
|
||||||
|
return this[kData].message_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of messages ever sent in a thread, it's similar to message_count on message creation,
|
||||||
|
* but will not decrement the number when a message is deleted.
|
||||||
|
*/
|
||||||
|
public get totalMessageSent() {
|
||||||
|
return this[kData].total_message_sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel is a thread channel
|
||||||
|
*/
|
||||||
|
public isThread(): this is ThreadChannelMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
|
||||||
|
export interface ThreadOnlyChannelMixin<
|
||||||
|
Type extends ChannelType.GuildForum | ChannelType.GuildMedia = ChannelType.GuildForum | ChannelType.GuildMedia,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @remarks has an array of sub-structures {@link ForumTag} that extending mixins should add to their DataTemplate and _optimizeData
|
||||||
|
*/
|
||||||
|
export class ThreadOnlyChannelMixin<
|
||||||
|
Type extends ChannelType.GuildForum | ChannelType.GuildMedia = ChannelType.GuildForum | ChannelType.GuildMedia,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The emoji to show in the add reaction button on a thread in this channel.
|
||||||
|
*/
|
||||||
|
public get defaultReactionEmoji() {
|
||||||
|
return this[kData].default_reaction_emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default sort order type used to order posts in this channel.
|
||||||
|
*
|
||||||
|
* @defaultValue `null` – indicates a preferred sort order hasn't been set.
|
||||||
|
*/
|
||||||
|
public get defaultSortOrder() {
|
||||||
|
return this[kData].default_sort_order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel only allows thread creation
|
||||||
|
*/
|
||||||
|
public isThreadOnly(): this is ThreadOnlyChannelMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/structures/src/channels/mixins/VoiceChannelMixin.ts
Normal file
51
packages/structures/src/channels/mixins/VoiceChannelMixin.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ChannelType } from 'discord-api-types/v10';
|
||||||
|
import { kData } from '../../utils/symbols.js';
|
||||||
|
import type { Channel } from '../Channel.js';
|
||||||
|
import { TextChannelMixin } from './TextChannelMixin.js';
|
||||||
|
|
||||||
|
export interface VoiceChannelMixin<
|
||||||
|
Type extends ChannelType.GuildStageVoice | ChannelType.GuildVoice =
|
||||||
|
| ChannelType.GuildStageVoice
|
||||||
|
| ChannelType.GuildVoice,
|
||||||
|
> extends Channel<Type> {}
|
||||||
|
|
||||||
|
export class VoiceChannelMixin<
|
||||||
|
Type extends ChannelType.GuildStageVoice | ChannelType.GuildVoice =
|
||||||
|
| ChannelType.GuildStageVoice
|
||||||
|
| ChannelType.GuildVoice,
|
||||||
|
> extends TextChannelMixin<Type> {
|
||||||
|
/**
|
||||||
|
* The bitrate (in bits) of the voice channel.
|
||||||
|
*/
|
||||||
|
public get bitrate() {
|
||||||
|
return this[kData].bitrate!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The voice region id for this channel, automatic when set to null.
|
||||||
|
*/
|
||||||
|
public get rtcRegion() {
|
||||||
|
return this[kData].rtc_region!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The camera video quality mode of the voice channel, {@link discord-api-types/v10#(VideoQualityMode:enum) | Auto} when not present.
|
||||||
|
*/
|
||||||
|
public get videoQualityMode() {
|
||||||
|
return this[kData].video_quality_mode!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user limit of the voice channel.
|
||||||
|
*/
|
||||||
|
public get userLimit() {
|
||||||
|
return this[kData].user_limit!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this channel has voice connection capabilities
|
||||||
|
*/
|
||||||
|
public override isVoiceBased(): this is VoiceChannelMixin & this {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/structures/src/channels/mixins/index.ts
Normal file
15
packages/structures/src/channels/mixins/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export * from './AppliedTagsMixin.js';
|
||||||
|
export * from './ChannelOwnerMixin.js';
|
||||||
|
export * from './ChannelParentMixin.js';
|
||||||
|
export * from './ChannelPermissionMixin.js';
|
||||||
|
export * from './ChannelPinMixin.js';
|
||||||
|
export * from './ChannelSlowmodeMixin.js';
|
||||||
|
export * from './ChannelTopicMixin.js';
|
||||||
|
export * from './ChannelWebhookMixin.js';
|
||||||
|
export * from './DMChannelMixin.js';
|
||||||
|
export * from './GroupDMMixin.js';
|
||||||
|
export * from './GuildChannelMixin.js';
|
||||||
|
export * from './TextChannelMixin.js';
|
||||||
|
export * from './ThreadChannelMixin.js';
|
||||||
|
export * from './ThreadOnlyChannelMixin.js';
|
||||||
|
export * from './VoiceChannelMixin.js';
|
||||||
9
packages/structures/src/index.ts
Normal file
9
packages/structures/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export * from './bitfields/index.js';
|
||||||
|
export * from './channels/index.js';
|
||||||
|
export * from './invites/index.js';
|
||||||
|
export * from './users/index.js';
|
||||||
|
export * from './Structure.js';
|
||||||
|
export * from './Mixin.js';
|
||||||
|
export * from './utils/optimization.js';
|
||||||
|
export type * from './utils/types.js';
|
||||||
|
export type * from './MixinTypes.d.ts';
|
||||||
220
packages/structures/src/invites/Invite.ts
Normal file
220
packages/structures/src/invites/Invite.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import { type APIInvite, type APIExtendedInvite, RouteBases } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { kData, kExpiresTimestamp, kCreatedTimestamp, kPatch } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
export interface APIActualInvite extends APIInvite, Partial<Omit<APIExtendedInvite, keyof APIInvite>> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an invitation to a Discord channel
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
*/
|
||||||
|
export class Invite<Omitted extends keyof APIActualInvite | '' = 'created_at' | 'expires_at'> extends Structure<
|
||||||
|
APIActualInvite,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each Invite
|
||||||
|
*
|
||||||
|
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
|
||||||
|
* use `Object.defineProperties`. To override the defaults, set this value directly.
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIActualInvite> = {
|
||||||
|
set created_at(_: string) {},
|
||||||
|
set expires_at(_: string) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized storage of {@link discord-api-types/v10#(APIActualInvite:interface).expires_at}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected [kExpiresTimestamp]: number | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized storage of {@link discord-api-types/v10#(APIActualInvite:interface).created_at}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected [kCreatedTimestamp]: number | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data - The raw data received from the API for the invite
|
||||||
|
*/
|
||||||
|
public constructor(data: Partialize<APIActualInvite, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
this.optimizeData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.[kPatch]}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public override [kPatch](data: Partial<APIActualInvite>) {
|
||||||
|
super[kPatch](data);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.optimizeData}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected override optimizeData(data: Partial<APIActualInvite>) {
|
||||||
|
if (data.expires_at) {
|
||||||
|
this[kExpiresTimestamp] = Date.parse(data.expires_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.created_at) {
|
||||||
|
this[kCreatedTimestamp] = Date.parse(data.created_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The code for this invite
|
||||||
|
*/
|
||||||
|
public get code() {
|
||||||
|
return this[kData].code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target type (for voice channel invites)
|
||||||
|
*/
|
||||||
|
public get targetType() {
|
||||||
|
return this[kData].target_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this invite
|
||||||
|
*/
|
||||||
|
public get type() {
|
||||||
|
return this[kData].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The approximate number of online members of the guild this invite is for
|
||||||
|
*
|
||||||
|
* @remarks Only available when the invite was fetched from `GET /invites/<code>` with counts
|
||||||
|
*/
|
||||||
|
public get approximatePresenceCount() {
|
||||||
|
return this[kData].approximate_presence_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The approximate total number of members of the guild this invite is for
|
||||||
|
*
|
||||||
|
* @remarks Only available when the invite was fetched from `GET /invites/<code>` with counts
|
||||||
|
*/
|
||||||
|
public get approximateMemberCount() {
|
||||||
|
return this[kData].approximate_member_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp this invite will expire at
|
||||||
|
*/
|
||||||
|
public get expiresTimestamp() {
|
||||||
|
if (this[kExpiresTimestamp]) {
|
||||||
|
return this[kExpiresTimestamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdTimestamp = this.createdTimestamp;
|
||||||
|
const maxAge = this.maxAge;
|
||||||
|
if (createdTimestamp && maxAge) {
|
||||||
|
this[kExpiresTimestamp] = createdTimestamp + (maxAge as number) * 1_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this[kExpiresTimestamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the invite will expire at
|
||||||
|
*/
|
||||||
|
public get expiresAt() {
|
||||||
|
const expiresTimestamp = this.expiresTimestamp;
|
||||||
|
return expiresTimestamp ? new Date(expiresTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times this invite has been used
|
||||||
|
*/
|
||||||
|
public get uses() {
|
||||||
|
return this[kData].uses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of times this invite can be used
|
||||||
|
*/
|
||||||
|
public get maxUses() {
|
||||||
|
return this[kData].max_uses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum age of the invite, in seconds, 0 for non-expiring
|
||||||
|
*/
|
||||||
|
public get maxAge() {
|
||||||
|
return this[kData].max_age;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this invite only grants temporary membership
|
||||||
|
*/
|
||||||
|
public get temporary() {
|
||||||
|
return this[kData].temporary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp this invite was created at
|
||||||
|
*/
|
||||||
|
public get createdTimestamp() {
|
||||||
|
return this[kCreatedTimestamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the invite was created at
|
||||||
|
*/
|
||||||
|
public get createdAt() {
|
||||||
|
const createdTimestamp = this.createdTimestamp;
|
||||||
|
return createdTimestamp ? new Date(createdTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the invite
|
||||||
|
*/
|
||||||
|
public get url() {
|
||||||
|
return this.code ? `${RouteBases.invite}/${this.code}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When concatenated with a string, this automatically concatenates the invite's URL instead of the object.
|
||||||
|
*
|
||||||
|
* @returns The URL to the invite or an empty string if it doesn't have a code
|
||||||
|
*/
|
||||||
|
public override toString() {
|
||||||
|
return this.url ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON() {
|
||||||
|
const clone = super.toJSON();
|
||||||
|
if (this[kExpiresTimestamp]) {
|
||||||
|
clone.expires_at = new Date(this[kExpiresTimestamp]).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this[kCreatedTimestamp]) {
|
||||||
|
clone.created_at = new Date(this[kCreatedTimestamp]).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the primitive value of the specified object.
|
||||||
|
*/
|
||||||
|
public override valueOf() {
|
||||||
|
return this.code ?? super.valueOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/structures/src/invites/index.ts
Normal file
1
packages/structures/src/invites/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Invite.js';
|
||||||
40
packages/structures/src/users/AvatarDecorationData.ts
Normal file
40
packages/structures/src/users/AvatarDecorationData.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { APIAvatarDecorationData } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { kData } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents metadata of an avatar decoration of a User.
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
*/
|
||||||
|
export class AvatarDecorationData<Omitted extends keyof APIAvatarDecorationData | '' = ''> extends Structure<
|
||||||
|
APIAvatarDecorationData,
|
||||||
|
Omitted
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each Connection
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIAvatarDecorationData> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data - The raw data received from the API for the connection
|
||||||
|
*/
|
||||||
|
public constructor(data: Partialize<APIAvatarDecorationData, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the SKU this avatar decoration is part of.
|
||||||
|
*/
|
||||||
|
public get skuId() {
|
||||||
|
return this[kData].sku_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The asset of this avatar decoration.
|
||||||
|
*/
|
||||||
|
public get asset() {
|
||||||
|
return this[kData].asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
packages/structures/src/users/Connection.ts
Normal file
95
packages/structures/src/users/Connection.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { APIConnection } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { kData, kPatch } from '../utils/symbols.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a user's connection on Discord.
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
*/
|
||||||
|
export class Connection<Omitted extends keyof APIConnection | '' = ''> extends Structure<APIConnection, Omitted> {
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each Connection
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIConnection> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data - The raw data received from the API for the connection
|
||||||
|
*/
|
||||||
|
public constructor(data: Partialize<APIConnection, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.[kPatch]}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public override [kPatch](data: Partial<APIConnection>) {
|
||||||
|
return super[kPatch](data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the connection account
|
||||||
|
*/
|
||||||
|
public get id() {
|
||||||
|
return this[kData].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of the connection account
|
||||||
|
*/
|
||||||
|
public get name() {
|
||||||
|
return this[kData].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of service this connection is for
|
||||||
|
*/
|
||||||
|
public get type() {
|
||||||
|
return this[kData].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the connection is revoked
|
||||||
|
*/
|
||||||
|
public get revoked() {
|
||||||
|
return this[kData].revoked ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the connection is verified
|
||||||
|
*/
|
||||||
|
public get verified() {
|
||||||
|
return this[kData].verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether friend sync is enabled for this connection
|
||||||
|
*/
|
||||||
|
public get friendSync() {
|
||||||
|
return this[kData].friend_sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether activities related to this connection are shown in the users presence
|
||||||
|
*/
|
||||||
|
public get showActivity() {
|
||||||
|
return this[kData].show_activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this connection has an Oauth2 token for console voice transfer
|
||||||
|
*/
|
||||||
|
public get twoWayLink() {
|
||||||
|
return this[kData].two_way_link;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The visibility state for this connection
|
||||||
|
*/
|
||||||
|
public get visibility() {
|
||||||
|
return this[kData].visibility;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
packages/structures/src/users/User.ts
Normal file
180
packages/structures/src/users/User.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||||
|
import type { APIUser } from 'discord-api-types/v10';
|
||||||
|
import { Structure } from '../Structure.js';
|
||||||
|
import { kData, kPatch } from '../utils/symbols.js';
|
||||||
|
import { isIdSet } from '../utils/type-guards.js';
|
||||||
|
import type { Partialize } from '../utils/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents any user on Discord.
|
||||||
|
*
|
||||||
|
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
|
||||||
|
* @remarks has a substructure `AvatarDecorationData`, which needs to be instantiated and stored by an extending class using it
|
||||||
|
*/
|
||||||
|
export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<APIUser, Omitted> {
|
||||||
|
/**
|
||||||
|
* The template used for removing data from the raw data stored for each User
|
||||||
|
*/
|
||||||
|
public static override readonly DataTemplate: Partial<APIUser> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data - The raw data received from the API for the user
|
||||||
|
*/
|
||||||
|
public constructor(data: Partialize<APIUser, Omitted>) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc Structure.[kPatch]}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public override [kPatch](data: Partial<APIUser>) {
|
||||||
|
return super[kPatch](data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's id
|
||||||
|
*/
|
||||||
|
public get id() {
|
||||||
|
return this[kData].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of the user
|
||||||
|
*/
|
||||||
|
public get username() {
|
||||||
|
return this[kData].username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's 4 digit tag, if a bot
|
||||||
|
*/
|
||||||
|
public get discriminator() {
|
||||||
|
return this[kData].discriminator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's display name, the application name for bots
|
||||||
|
*/
|
||||||
|
public get globalName() {
|
||||||
|
return this[kData].global_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name displayed in the client for this user when no nickname is set
|
||||||
|
*/
|
||||||
|
public get displayName() {
|
||||||
|
return this.globalName ?? this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user avatar's hash
|
||||||
|
*/
|
||||||
|
public get avatar() {
|
||||||
|
return this[kData].avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is a bot
|
||||||
|
*/
|
||||||
|
public get bot() {
|
||||||
|
return this[kData].bot ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is an Official Discord System user
|
||||||
|
*/
|
||||||
|
public get system() {
|
||||||
|
return this[kData].system ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user has mfa enabled
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identify` scope
|
||||||
|
*/
|
||||||
|
public get mfaEnabled() {
|
||||||
|
return this[kData].mfa_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's banner hash
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was manually fetched
|
||||||
|
*/
|
||||||
|
public get banner() {
|
||||||
|
return this[kData].banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base 10 accent color of the user's banner
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was manually fetched
|
||||||
|
*/
|
||||||
|
public get accentColor() {
|
||||||
|
return this[kData].accent_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's primary Discord language
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was fetched with an Oauth2 token and the `identify` scope
|
||||||
|
*/
|
||||||
|
public get locale() {
|
||||||
|
return this[kData].locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the email on the user's account has been verified
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `email` scope
|
||||||
|
*/
|
||||||
|
public get verified() {
|
||||||
|
return this[kData].verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's email
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `email` scope
|
||||||
|
*/
|
||||||
|
public get email() {
|
||||||
|
return this[kData].email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of nitro subscription on the user's account
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identify` scope
|
||||||
|
*/
|
||||||
|
public get premiumType() {
|
||||||
|
return this[kData].premium_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp the user was created at
|
||||||
|
*/
|
||||||
|
public get createdTimestamp() {
|
||||||
|
return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the user was created at
|
||||||
|
*/
|
||||||
|
public get createdAt() {
|
||||||
|
const createdTimestamp = this.createdTimestamp;
|
||||||
|
return createdTimestamp ? new Date(createdTimestamp) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hexadecimal version of the user accent color, with a leading hash
|
||||||
|
*
|
||||||
|
* @remarks This property is only set when the user was manually fetched
|
||||||
|
*/
|
||||||
|
public get hexAccentColor() {
|
||||||
|
const accentColor = this.accentColor;
|
||||||
|
if (typeof accentColor !== 'number') return accentColor;
|
||||||
|
return `#${accentColor.toString(16).padStart(6, '0')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/structures/src/users/index.ts
Normal file
3
packages/structures/src/users/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './AvatarDecorationData.js';
|
||||||
|
export * from './User.js';
|
||||||
|
export * from './Connection.js';
|
||||||
10
packages/structures/src/utils/optimization.ts
Normal file
10
packages/structures/src/utils/optimization.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function extendTemplate<SuperTemplate extends Record<string, unknown>>(
|
||||||
|
superTemplate: SuperTemplate,
|
||||||
|
additions: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> & SuperTemplate {
|
||||||
|
return Object.defineProperties(additions, Object.getOwnPropertyDescriptors(superTemplate)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> &
|
||||||
|
SuperTemplate;
|
||||||
|
}
|
||||||
15
packages/structures/src/utils/symbols.ts
Normal file
15
packages/structures/src/utils/symbols.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const kData = Symbol.for('djs.structures.data');
|
||||||
|
export const kClone = Symbol.for('djs.structures.clone');
|
||||||
|
export const kPatch = Symbol.for('djs.structures.patch');
|
||||||
|
export const kExpiresTimestamp = Symbol.for('djs.structures.expiresTimestamp');
|
||||||
|
export const kCreatedTimestamp = Symbol.for('djs.structures.createdTimestamp');
|
||||||
|
export const kEditedTimestamp = Symbol.for('djs.structures.editedTimestamp');
|
||||||
|
export const kArchiveTimestamp = Symbol.for('djs.structures.archiveTimestamp');
|
||||||
|
|
||||||
|
export const kAllow = Symbol.for('djs.structures.allow');
|
||||||
|
export const kDeny = Symbol.for('djs.structures.deny');
|
||||||
|
|
||||||
|
export const kLastPinTimestamp = Symbol.for('djs.structures.lastPinTimestamp');
|
||||||
|
|
||||||
|
export const kMixinConstruct = Symbol.for('djs.structures.mixin.construct');
|
||||||
|
export const kMixinToJSON = Symbol.for('djs.structures.mixin.toJSON');
|
||||||
3
packages/structures/src/utils/type-guards.ts
Normal file
3
packages/structures/src/utils/type-guards.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function isIdSet(id: unknown): id is bigint | string {
|
||||||
|
return typeof id === 'string' || typeof id === 'bigint';
|
||||||
|
}
|
||||||
36
packages/structures/src/utils/types.ts
Normal file
36
packages/structures/src/utils/types.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export type ReplaceOmittedWithUnknown<Omitted extends keyof Data | '', Data> = {
|
||||||
|
[Key in keyof Data]: Key extends Omitted ? unknown : Data[Key];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CollapseUnion<Type> = Type extends infer Union ? { [Key in keyof Union]: Union[Key] } : never;
|
||||||
|
|
||||||
|
export type OptionalPropertyNames<Type> = {
|
||||||
|
[Key in keyof Type]-?: {} extends { [Prop in Key]: Type[Key] } ? Key : never;
|
||||||
|
}[keyof Type];
|
||||||
|
|
||||||
|
export type MergePrototype<Class1, Class2> = Pick<Class1, Exclude<keyof Class1, keyof Class2>> &
|
||||||
|
Pick<Class2, Exclude<keyof Class2, OptionalPropertyNames<Class2>>> &
|
||||||
|
Pick<Class2, Exclude<OptionalPropertyNames<Class2>, keyof Class1>> & {
|
||||||
|
[Prop in OptionalPropertyNames<Class2> & keyof Class1]: Class1[Prop] | Exclude<Class2[Prop], undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MergePrototypes<ClassArray extends readonly unknown[]> = ClassArray extends [infer Class1]
|
||||||
|
? Class1
|
||||||
|
: ClassArray extends [infer Class1, ...infer Rest]
|
||||||
|
? MergePrototype<Class1, MergePrototypes<Rest>>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export interface RecursiveReadonlyArray<ItemType> extends ReadonlyArray<ItemType | RecursiveReadonlyArray<ItemType>> {}
|
||||||
|
|
||||||
|
export type EnumLike<Enum, Value> = Record<keyof Enum, Value>;
|
||||||
|
|
||||||
|
export type If<Check, Value, True, False = never> = Check extends Value ? (Value extends Check ? True : False) : False;
|
||||||
|
|
||||||
|
export type NonAbstract<Type extends abstract new (...args: any) => any> = Type extends abstract new (
|
||||||
|
...args: infer Args
|
||||||
|
) => infer Instance
|
||||||
|
? Pick<Type, keyof Type> & (new (...args: Args) => Instance)
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type Partialize<Type, Omitted extends keyof Type | ''> = Omit<Type, Omitted> &
|
||||||
|
Partial<Pick<Type, Exclude<Omitted, ''>>>;
|
||||||
9
packages/structures/tsconfig.docs.json
Normal file
9
packages/structures/tsconfig.docs.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||||
|
"extends": "../../tsconfig.docs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist-docs"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
22
packages/structures/tsconfig.eslint.json
Normal file
22
packages/structures/tsconfig.eslint.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"*.ts",
|
||||||
|
"*.js",
|
||||||
|
"*.cjs",
|
||||||
|
"*.mjs",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.js",
|
||||||
|
"src/**/*.cjs",
|
||||||
|
"src/**/*.mjs",
|
||||||
|
"bin",
|
||||||
|
"scripts",
|
||||||
|
"__tests__",
|
||||||
|
"__mocks__"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
9
packages/structures/tsconfig.json
Normal file
9
packages/structures/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.cjs", "src/**/*.mjs", "bin"],
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": false
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/structures/tsconfig.test.json
Normal file
10
packages/structures/tsconfig.test.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["__tests__/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
6
packages/structures/tsup.config.ts
Normal file
6
packages/structures/tsup.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
|
||||||
|
import { createTsupConfig } from '../../tsup.config.js';
|
||||||
|
|
||||||
|
export default createTsupConfig({
|
||||||
|
esbuildPlugins: [esbuildPluginVersionInjector()],
|
||||||
|
});
|
||||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -1544,6 +1544,73 @@ importers:
|
|||||||
specifier: ~5.8.3
|
specifier: ~5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
|
|
||||||
|
packages/structures:
|
||||||
|
dependencies:
|
||||||
|
'@discordjs/formatters':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../formatters
|
||||||
|
'@sapphire/snowflake':
|
||||||
|
specifier: ^3.5.5
|
||||||
|
version: 3.5.5
|
||||||
|
discord-api-types:
|
||||||
|
specifier: ^0.38.15
|
||||||
|
version: 0.38.15
|
||||||
|
devDependencies:
|
||||||
|
'@discordjs/api-extractor':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../api-extractor
|
||||||
|
'@discordjs/scripts':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../scripts
|
||||||
|
'@favware/cliff-jumper':
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.15.2
|
||||||
|
version: 22.15.26
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.1.4(vitest@3.1.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@22.15.26)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0))
|
||||||
|
cpy-cli:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.0
|
||||||
|
cross-env:
|
||||||
|
specifier: ^7.0.3
|
||||||
|
version: 7.0.3
|
||||||
|
esbuild-plugin-version-injector:
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
|
eslint:
|
||||||
|
specifier: ^9.25.1
|
||||||
|
version: 9.27.0(jiti@2.4.2)
|
||||||
|
eslint-config-neon:
|
||||||
|
specifier: ^0.2.7
|
||||||
|
version: 0.2.7(@typescript-eslint/types@8.33.0)(@typescript-eslint/utils@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
|
eslint-formatter-compact:
|
||||||
|
specifier: ^8.40.0
|
||||||
|
version: 8.40.0
|
||||||
|
eslint-formatter-pretty:
|
||||||
|
specifier: ^6.0.1
|
||||||
|
version: 6.0.1
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.5.3
|
||||||
|
version: 3.5.3
|
||||||
|
tsd:
|
||||||
|
specifier: ^0.31.2
|
||||||
|
version: 0.31.2
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.4.0
|
||||||
|
version: 8.5.0(@microsoft/api-extractor@7.52.3(@types/node@22.15.26))(jiti@2.4.2)(postcss@8.5.4)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.0)
|
||||||
|
turbo:
|
||||||
|
specifier: ^2.5.2
|
||||||
|
version: 2.5.3
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.8.3
|
||||||
|
version: 5.8.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.1.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@22.15.26)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0)
|
||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-icons/all-files':
|
'@react-icons/all-files':
|
||||||
@@ -6464,9 +6531,6 @@ packages:
|
|||||||
'@types/estree-jsx@1.0.5':
|
'@types/estree-jsx@1.0.5':
|
||||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||||
|
|
||||||
'@types/estree@1.0.6':
|
|
||||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
|
||||||
|
|
||||||
'@types/estree@1.0.7':
|
'@types/estree@1.0.7':
|
||||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||||
|
|
||||||
@@ -16759,7 +16823,7 @@ snapshots:
|
|||||||
proc-log: 4.2.0
|
proc-log: 4.2.0
|
||||||
promise-inflight: 1.0.1
|
promise-inflight: 1.0.1
|
||||||
promise-retry: 2.0.1
|
promise-retry: 2.0.1
|
||||||
semver: 7.7.2
|
semver: 7.6.3
|
||||||
which: 4.0.0
|
which: 4.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bluebird
|
- bluebird
|
||||||
@@ -16781,7 +16845,7 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 3.0.2
|
json-parse-even-better-errors: 3.0.2
|
||||||
normalize-package-data: 6.0.2
|
normalize-package-data: 6.0.2
|
||||||
proc-log: 4.2.0
|
proc-log: 4.2.0
|
||||||
semver: 7.7.2
|
semver: 7.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bluebird
|
- bluebird
|
||||||
|
|
||||||
@@ -20156,8 +20220,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.7
|
'@types/estree': 1.0.7
|
||||||
|
|
||||||
'@types/estree@1.0.6': {}
|
|
||||||
|
|
||||||
'@types/estree@1.0.7': {}
|
'@types/estree@1.0.7': {}
|
||||||
|
|
||||||
'@types/express-serve-static-core@4.19.6':
|
'@types/express-serve-static-core@4.19.6':
|
||||||
@@ -20737,7 +20799,7 @@ snapshots:
|
|||||||
|
|
||||||
'@typescript-eslint/utils@7.18.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/utils@7.18.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.27.0(jiti@2.4.2))
|
'@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2))
|
||||||
'@typescript-eslint/scope-manager': 7.18.0
|
'@typescript-eslint/scope-manager': 7.18.0
|
||||||
'@typescript-eslint/types': 7.18.0
|
'@typescript-eslint/types': 7.18.0
|
||||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3)
|
||||||
@@ -24333,7 +24395,7 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
@@ -27248,7 +27310,7 @@ snapshots:
|
|||||||
|
|
||||||
npm-install-checks@6.3.0:
|
npm-install-checks@6.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.2
|
semver: 7.6.3
|
||||||
|
|
||||||
npm-normalize-package-bin@3.0.1: {}
|
npm-normalize-package-bin@3.0.1: {}
|
||||||
|
|
||||||
@@ -27256,7 +27318,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hosted-git-info: 7.0.2
|
hosted-git-info: 7.0.2
|
||||||
proc-log: 4.2.0
|
proc-log: 4.2.0
|
||||||
semver: 7.7.2
|
semver: 7.6.3
|
||||||
validate-npm-package-name: 5.0.1
|
validate-npm-package-name: 5.0.1
|
||||||
|
|
||||||
npm-package-arg@12.0.1:
|
npm-package-arg@12.0.1:
|
||||||
@@ -27271,7 +27333,7 @@ snapshots:
|
|||||||
npm-install-checks: 6.3.0
|
npm-install-checks: 6.3.0
|
||||||
npm-normalize-package-bin: 3.0.1
|
npm-normalize-package-bin: 3.0.1
|
||||||
npm-package-arg: 11.0.3
|
npm-package-arg: 11.0.3
|
||||||
semver: 7.7.2
|
semver: 7.6.3
|
||||||
|
|
||||||
npm-registry-fetch@18.0.2:
|
npm-registry-fetch@18.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user