mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-02 17:00:08 +00:00
15
.devcontainer/Dockerfile
Normal file
15
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian-10
|
||||
|
||||
ENV DENO_INSTALL=/deno
|
||||
RUN mkdir -p /deno \
|
||||
&& curl -fsSL https://deno.land/x/install/install.sh | sh \
|
||||
&& chown -R vscode /deno
|
||||
|
||||
ENV PATH=${DENO_INSTALL}/bin:${PATH} \
|
||||
DENO_DIR=${DENO_INSTALL}/.cache/deno
|
||||
|
||||
RUN deno cache deps.ts
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Deno",
|
||||
"dockerFile": "Dockerfile",
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash"
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"denoland.vscode-deno"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
|
||||
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
|
||||
|
||||
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
||||
// "remoteUser": "vscode"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
76
.github/CODE_OF_CONDUCT.md
vendored
76
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at https://discord.gg/SZNuut. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
10
.github/ISSUE_TEMPLATE/ask-a-question.md
vendored
10
.github/ISSUE_TEMPLATE/ask-a-question.md
vendored
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Ask A Question
|
||||
about: Use this to ask your question
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: Skillz4Killz, resynth1943
|
||||
|
||||
---
|
||||
|
||||
<!-- Please provide as much information as you can when asking. The more information you provide the more I can help. -->
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: Skillz4Killz
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. Please also list the version/branch/commit etc that you are using.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: Skillz4Killz, resynth1943
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -1,8 +1,5 @@
|
||||
name: Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -12,6 +9,7 @@ jobs:
|
||||
- name: Cache dependencies
|
||||
run: deno cache mod.ts
|
||||
- name: Run test script
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: deno test -A
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,12 +1,19 @@
|
||||
configs.ts
|
||||
# Allows quick testing of changes and keeps stuff like tokens private
|
||||
debug.ts
|
||||
|
||||
.DS_Store
|
||||
.lock
|
||||
node_modules
|
||||
|
||||
# Node modules - npm
|
||||
node_modules/
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
|
||||
# Docs
|
||||
dist/
|
||||
dist/
|
||||
public/
|
||||
.cache/
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
|
||||
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"deno.import_intellisense_origins": {
|
||||
"https://deno.land": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||
"deno.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"deno.import_intellisense_origins": {
|
||||
"https://deno.land": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||
}
|
||||
|
||||
33
README.md
33
README.md
@@ -7,22 +7,19 @@
|
||||

|
||||
|
||||
- First-class TypeScript & JavaScript support
|
||||
- Security & stable
|
||||
- Secure & stable
|
||||
- Builtin Documentation
|
||||
- Minimalistic
|
||||
- Functional API
|
||||
- Actively maintained
|
||||
- Function-based API
|
||||
|
||||
### Beginner Developers
|
||||
|
||||
Don't worry a lot of developers start out coding their first projects as a Discord bot (I did 😉) and it is not so easy to do so. Discordeno is built considering all the issues with pre-existing libraries and issues that I had when I first started out coding bots.
|
||||
Don't worry a lot of developers start out coding their first projects as a Discord bot (I did 😉) and it is not so easy to do so. Discordeno is built considering all the issues with pre-existing libraries and issues that I had when I first started out coding bots.
|
||||
If you are a beginner developer, you may check out these awesome official and unofficial boilerplates:
|
||||
|
||||
- Official Discordeno Boilerplate
|
||||
- [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template)
|
||||
- [Features](https://github.com/Skillz4Killz/Discordeno-bot-template#features)
|
||||
- Dencord Starter
|
||||
- [GitHub](https://github.com/ayntee/dencord-starter)
|
||||
|
||||
If you do not wish to use a boilerplate, you may continue reading.
|
||||
|
||||
@@ -31,36 +28,32 @@ If you do not wish to use a boilerplate, you may continue reading.
|
||||
Here's a minimal example to get started with:
|
||||
|
||||
```typescript
|
||||
import startBot, { sendMessage, Intents } from "https://deno.land/x/discordeno@9.4.0/mod.ts";
|
||||
import { Intents, startBot } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
|
||||
|
||||
startBot({
|
||||
token: "BOT TOKEN",
|
||||
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
|
||||
intents: [Intents.GUILDS, Intents.GUILD_MESSAGES],
|
||||
eventHandlers: {
|
||||
ready: () => console.log('Successfully connected to gateway'),
|
||||
messageCreate: (message) => {
|
||||
if (message.content === "hello") {
|
||||
sendMessage(message.channelID, "Hi there!");
|
||||
ready() {
|
||||
console.log("Successfully connected to gateway");
|
||||
},
|
||||
messageCreate(message) {
|
||||
if (message.content === "!ping") {
|
||||
message.reply("Pong using Discordeno!");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Useful Links
|
||||
|
||||
- [Website](https://discordeno.mod.land)
|
||||
- [Documentation](https://doc.deno.land/https/deno.land/x/discordeno/mod.ts)
|
||||
- [Support server](https://discord.com/invite/5vBgXk3UcZ)
|
||||
- [Contributing Guide](https://github.com/discordeno/discordeno/blob/master/.github/CONTRIBUTING.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Discordeno expects participants to adhere to our [Code of Conduct](https://github.com/discordeno/discordeno/blob/master/.github/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Contributing Guide
|
||||
|
||||
We appreciate your help!
|
||||
|
||||
Before contributing, please read the [Contributing Guide](https://github.com/discordeno/discordeno/blob/master/.github/CONTRIBUTING.md).
|
||||
|
||||
16
deps.ts
16
deps.ts
@@ -1,15 +1 @@
|
||||
export {
|
||||
connectWebSocket,
|
||||
isWebSocketCloseEvent,
|
||||
isWebSocketPingEvent,
|
||||
isWebSocketPongEvent,
|
||||
} from "https://deno.land/std@0.67.0/ws/mod.ts";
|
||||
export type { WebSocket } from "https://deno.land/std@0.67.0/ws/mod.ts";
|
||||
export { delay } from "https://deno.land/std@0.75.0/async/delay.ts";
|
||||
export { encode } from "https://deno.land/std@0.75.0/encoding/base64.ts";
|
||||
export {
|
||||
assert,
|
||||
assertArrayIncludes,
|
||||
assertEquals,
|
||||
} from "https://deno.land/std@0.75.0/testing/asserts.ts";
|
||||
export { decompress_with as inflate } from "https://unpkg.com/@evan/wasm@0.0.12/target/zlib/deno.js";
|
||||
export { encode } from "https://deno.land/std@0.81.0/encoding/base64.ts";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "discordeno",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "Docs for Discordeno",
|
||||
"repository": "https://github.com/discordeno/discordeno",
|
||||
@@ -7,12 +8,9 @@
|
||||
"docs:dev": "vuepress dev src --host localhost",
|
||||
"docs:build": "vuepress build src"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"vuepress": "^1.5.3",
|
||||
"vuepress-theme-yuu": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"vuepress": "^1.7.1",
|
||||
"vuepress-theme-yuu": "^2.3.0",
|
||||
"@vuepress/plugin-back-to-top": "^1.7.1",
|
||||
"@vuepress/plugin-medium-zoom": "^1.7.1"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { name, description, repository } = require("../../package");
|
||||
const { name, description, repository: repo } = require("../../package");
|
||||
const nav = require("./navbar");
|
||||
const sidebar = require("./sidebar");
|
||||
|
||||
const title = name[0].toUpperCase() + name.slice(1);
|
||||
const discordLink = "https://discord.com/invite/5vBgXk3UcZ";
|
||||
|
||||
module.exports = {
|
||||
base: `/`,
|
||||
@@ -29,65 +30,16 @@ module.exports = {
|
||||
],
|
||||
theme: "yuu",
|
||||
themeConfig: {
|
||||
repo: repository,
|
||||
repo,
|
||||
docsDir: "docs/src",
|
||||
editLinks: true,
|
||||
lastUpdated: true,
|
||||
sidebarDepth: 0,
|
||||
nav: [
|
||||
{
|
||||
text: "Home",
|
||||
link: "/",
|
||||
},
|
||||
{
|
||||
text: "Docs",
|
||||
link: "https://doc.deno.land/https/deno.land/x/discordeno/mod.ts",
|
||||
},
|
||||
{
|
||||
text: "Discord",
|
||||
link: discordLink,
|
||||
target: "_blank",
|
||||
},
|
||||
],
|
||||
sidebar: {
|
||||
"/": [
|
||||
{
|
||||
title: "Home",
|
||||
children: [
|
||||
"/",
|
||||
"faq",
|
||||
"gettingstarted",
|
||||
"djs",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Step By Step Guide",
|
||||
children: [
|
||||
"/stepbystep/",
|
||||
"/stepbystep/createbot",
|
||||
"/stepbystep/createcommand",
|
||||
"/stepbystep/createevent",
|
||||
"/stepbystep/createlanguage",
|
||||
"/stepbystep/createmonitor",
|
||||
"/stepbystep/createinhibitor",
|
||||
"/stepbystep/createtask",
|
||||
"/stepbystep/hostingbot",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Advanced Guide",
|
||||
children: [
|
||||
"/advanced/",
|
||||
"/advanced/arguments",
|
||||
"/advanced/customizations",
|
||||
"/advanced/dockerhosting",
|
||||
"/advanced/dynamiccommands",
|
||||
"/advanced/permlevels",
|
||||
"/advanced/subcommands",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
nav,
|
||||
sidebar,
|
||||
yuu: {
|
||||
defaultDarkTheme: true
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
"@vuepress/plugin-back-to-top",
|
||||
|
||||
15
docs/src/.vuepress/navbar.js
Normal file
15
docs/src/.vuepress/navbar.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = [
|
||||
{
|
||||
text: "Home",
|
||||
link: "/",
|
||||
},
|
||||
{
|
||||
text: "Docs",
|
||||
link: "https://doc.deno.land/https/deno.land/x/discordeno/mod.ts",
|
||||
},
|
||||
{
|
||||
text: "Discord",
|
||||
link: "https://discord.gg/5vBgXk3UcZ",
|
||||
target: "_blank",
|
||||
},
|
||||
]
|
||||
39
docs/src/.vuepress/sidebar.js
Normal file
39
docs/src/.vuepress/sidebar.js
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
"/": [
|
||||
{
|
||||
title: "Home",
|
||||
children: [
|
||||
"/",
|
||||
"faq",
|
||||
"gettingstarted",
|
||||
"djs",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Step By Step Guide",
|
||||
children: [
|
||||
"/stepbystep/",
|
||||
"/stepbystep/createbot",
|
||||
"/stepbystep/createcommand",
|
||||
"/stepbystep/createevent",
|
||||
"/stepbystep/createlanguage",
|
||||
"/stepbystep/createmonitor",
|
||||
"/stepbystep/createinhibitor",
|
||||
"/stepbystep/createtask",
|
||||
"/stepbystep/hostingbot",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Advanced Guide",
|
||||
children: [
|
||||
"/advanced/",
|
||||
"/advanced/arguments",
|
||||
"/advanced/customizations",
|
||||
"/advanced/dockerhosting",
|
||||
"/advanced/dynamiccommands",
|
||||
"/advanced/permlevels",
|
||||
"/advanced/subcommands",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -18,7 +18,7 @@ A Third Party Deno library for interacting with the Discord API.
|
||||
|
||||
### Best TypeScript Support!
|
||||
|
||||
First class support for Typescript! Never compile your code again in order to run it. Automated typings so they are never inaccurate or out of date.
|
||||
First class support for Typescript! Never compile your code again in order to run it.
|
||||
|
||||
### Stable Library
|
||||
|
||||
@@ -30,7 +30,7 @@ Several bot boilerplates are available to get up and running very quickly. The O
|
||||
|
||||
### Security
|
||||
|
||||
Checks all missing permissions necessary before sending a request to the API so that your bot's token do not get globally banned by Discord. Discordeno does not support self-bot functionality like other libraries either.
|
||||
Checks all missing permissions necessary before sending a request to the API so that your bot's token do not get globally banned by Discord. Discordeno does not support self-bot functionality like other libraries either. Even goes so far as to prevent self-bot farms!
|
||||
|
||||
### Documentation
|
||||
|
||||
|
||||
168
docs/src/djs.md
168
docs/src/djs.md
@@ -12,7 +12,7 @@ For the purposes of this guide, I will be using the current [latest commit](http
|
||||
|
||||
## Preparations
|
||||
|
||||
- First, create a Discordeno Bot using the [Generator Boilerplate](https://github.com/Skillz4Killz/Discordeno-bot-template) I will name it Zodiac.
|
||||
- First, create a Discordeno Bot using the [Generator Boilerplate](https://github.com/discordeno/discordeno-bot-template) I will name it Zodiac.
|
||||
|
||||
- Then `git clone https://github.com/Skillz4Killz/Zodiac.git`
|
||||
|
||||
@@ -100,54 +100,49 @@ client.login(config.env.TOKEN)
|
||||
Discordeno Version:
|
||||
|
||||
```ts
|
||||
import Client, {
|
||||
updateEventHandlers,
|
||||
} from "https://deno.land/x/discordeno@9.4.0/src/module/client.ts";
|
||||
import { botCache, Intents } from "./deps.ts";
|
||||
import { configs } from "./configs.ts";
|
||||
import { Intents } from "https://deno.land/x/discordeno@9.4.0/src/types/options.ts";
|
||||
import { eventHandlers } from "./src/events/eventHandlers.ts";
|
||||
import { Message } from "https://deno.land/x/discordeno@9.4.0/src/structures/message.ts";
|
||||
import { Command } from "./src/types/commands.ts";
|
||||
import { Guild } from "https://deno.land/x/discordeno@9.4.0/src/structures/guild.ts";
|
||||
import { importDirectory } from "./src/utils/helpers.ts";
|
||||
import { loadLanguages } from "./src/utils/i18next.ts";
|
||||
|
||||
export const botCache = {
|
||||
commands: new Map<string, Command>(),
|
||||
commandAliases: new Map<string, string>(),
|
||||
guildPrefixes: new Map<string, string>(),
|
||||
inhibitors: new Map<
|
||||
string,
|
||||
(message: Message, command: Command, guild?: Guild) => boolean
|
||||
>(),
|
||||
eventHandlers: {} as EventHandlers
|
||||
};
|
||||
console.info(
|
||||
"Beginning Bot Startup Process. This can take a little bit depending on your system. Loading now...",
|
||||
);
|
||||
|
||||
const importDirectory = async (path: string) => {
|
||||
const files = Deno.readDirSync(Deno.realPathSync(path));
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.name) continue;
|
||||
|
||||
const currentPath = `${path}/${file.name}`;
|
||||
if (file.isFile) {
|
||||
await import(currentPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
importDirectory(currentPath);
|
||||
}
|
||||
};
|
||||
// Always require these files be processed before anything else
|
||||
await Promise.all([
|
||||
"./src/customizations/structures",
|
||||
].map(
|
||||
(path) => importDirectory(Deno.realPathSync(path)),
|
||||
));
|
||||
|
||||
// Forces deno to read all the files which will fill the commands/inhibitors cache etc.
|
||||
await Promise.all(
|
||||
["./src/commands", "./src/inhibitors", "./src/events"].map((path) => importDirectory(path)),
|
||||
[
|
||||
"./src/commands",
|
||||
"./src/inhibitors",
|
||||
"./src/events",
|
||||
"./src/arguments",
|
||||
"./src/monitors",
|
||||
"./src/tasks",
|
||||
"./src/permissionLevels",
|
||||
"./src/events",
|
||||
].map(
|
||||
(path) => importDirectory(Deno.realPathSync(path)),
|
||||
),
|
||||
);
|
||||
|
||||
// Loads languages
|
||||
await loadLanguages();
|
||||
await import("./src/database/database.ts");
|
||||
|
||||
Client({
|
||||
startBot({
|
||||
token: configs.token,
|
||||
// Pick the intents you wish to have for your bot.
|
||||
intents: [Intents.GUILDS, Intents.GUILD_MESSAGES],
|
||||
eventHandlers: botCache.eventHandlers
|
||||
// For instance, to work with guild message reactions, you will have to pass the Intents.GUILD_MESSAGE_REACTIONS intent to the array.
|
||||
intents: [Intents.GUILDS, Intents.GUILD_MESSAGES],
|
||||
// These are all your event handler functions. Imported from the events folder
|
||||
eventHandlers: botCache.eventHandlers,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -156,15 +151,35 @@ Something we haven't converted yet from the `main.js` files is the event listene
|
||||
In our `ready.ts` file we can add the `ready` event listener.
|
||||
|
||||
```ts
|
||||
import { botCache } from "../../mod.ts";
|
||||
import { configs } from "../../configs.ts";
|
||||
import { cache } from "https://deno.land/x/discordeno@9.4.0/src/utils/cache.ts";
|
||||
import { editBotsStatus, chooseRandom } from "https://deno.land/x/discordeno@9.4.0/src/utils/utils.ts";
|
||||
import { StatusType } from "https://deno.land/x/discordeno@9.4.0/src/types/discord.ts";
|
||||
import { ActivityType } from "https://deno.land/x/discordeno@9.4.0/src/types/activity.ts";
|
||||
import {
|
||||
botCache,
|
||||
cache,
|
||||
editBotsStatus,
|
||||
StatusTypes,
|
||||
ActivityType,
|
||||
chooseRandom
|
||||
} from "../../deps.ts";
|
||||
import { registerTasks } from "./../utils/taskHelper.ts";
|
||||
|
||||
botCache.eventHandlers.ready = function () {
|
||||
console.log(`[READY] Bot is online and ready in ${cache.guilds.size} guild(s)!`);
|
||||
editBotsStatus(
|
||||
StatusTypes.DoNotDisturb,
|
||||
"Discordeno Best Lib",
|
||||
ActivityType.Game
|
||||
);
|
||||
|
||||
console.log(`Loaded ${botCache.arguments.size} Argument(s)`);
|
||||
console.log(`Loaded ${botCache.commands.size} Command(s)`);
|
||||
console.log(`Loaded ${Object.keys(botCache.eventHandlers).length} Event(s)`);
|
||||
console.log(`Loaded ${botCache.inhibitors.size} Inhibitor(s)`);
|
||||
console.log(`Loaded ${botCache.monitors.size} Monitor(s)`);
|
||||
console.log(`Loaded ${botCache.tasks.size} Task(s)`);
|
||||
|
||||
registerTasks();
|
||||
|
||||
console.log(
|
||||
`[READY] Bot is online and ready in ${cache.guilds.size} guild(s)!`
|
||||
);
|
||||
|
||||
// list of activities that the bot goes through
|
||||
const activityArray = [`${configs.prefix}help | `];
|
||||
@@ -236,34 +251,32 @@ module.exports = class addRoleCommand extends Command {
|
||||
|
||||
This is how to do it with Discordeno:
|
||||
```ts
|
||||
import { botCache } from "../../mod.ts";
|
||||
import { addRole } from "https://deno.land/x/discordeno@9.4.0/src/handlers/member.ts";
|
||||
import { sendAlertResponse, sendResponse } from "../utils/helpers.ts";
|
||||
import { createCommand } from "./../../utils/helpers.ts";
|
||||
|
||||
botCache.commands.set(`addrole`, {
|
||||
createCommand({
|
||||
name: "role",
|
||||
// Oher ways to call the command
|
||||
aliases: ["addrole"],
|
||||
// Is the description used for 'help' command
|
||||
description: "Adds mentioned role to mentioned user.",
|
||||
// Prevents it from being used in dms
|
||||
guildOnly: true,
|
||||
botServerPermissions: ["ADMINISTRATOR", "MANAGE_ROLES"],
|
||||
userServerPermissions: ["MANAGE_ROLES"],
|
||||
execute: (message, _args, guild) => {
|
||||
const [member] = message.mentions();
|
||||
const [roleIDToAdd] = message.mentionRoles;
|
||||
const role = guild?.roles.get(roleIDToAdd)
|
||||
|
||||
arguments: [
|
||||
{ name: "member", type: "member" },
|
||||
{ name: "role", type: "role" }
|
||||
],
|
||||
execute: (message, args) => {
|
||||
// checking to see if the user has the role or not
|
||||
if (!member.roles.includes(roleIDToAdd)) {
|
||||
addRole(guild!, member.user.id, roleIDToAdd)
|
||||
sendAlertResponse(message, `has been given the role: ${role!.name}`, 5);
|
||||
if (!args.member.roles.includes(args.role.id)) {
|
||||
args.member.addRole(message.guildID, args.role.id)
|
||||
message.sendResponse(`${args.member.mention} has been given the role: ${args.role.name}`, 5);
|
||||
} else {
|
||||
sendResponse(message, `already has the role: ${role!.name}`)
|
||||
message.sendResponse(`${args.member.mention} already has the role: ${args.role.name}`)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// other ways to call the command
|
||||
createCommandAliases("role", "addrole");
|
||||
```
|
||||
|
||||
Awesome, that is a full command converted from Discord.JS to Discordeno. See how easy it is! Let's convert one more command to see how to really take full advantage of Discordeno boilerplate and have something amazing.
|
||||
@@ -304,7 +317,6 @@ module.exports = class kickCommand extends Command {
|
||||
userPermissions: ['KICK_MEMBERS'],
|
||||
// Prevents anyone other than owner to use the command
|
||||
ownerOnly: false
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -341,17 +353,11 @@ module.exports = class kickCommand extends Command {
|
||||
|
||||
Discordeno Version
|
||||
```ts
|
||||
import { sendMessage } from "https://deno.land/x/discordeno@9.4.0/src/handlers/channel.ts";
|
||||
import { Member } from "https://deno.land/x/discordeno@9.4.0/src/structures/member.ts";
|
||||
import { kick } from "https://deno.land/x/discordeno@9.4.0/src/handlers/member.ts";
|
||||
import { deleteMessage } from "https://deno.land/x/discordeno@9.4.0/src/handlers/message.ts";
|
||||
import { botCache } from "../../mod.ts";
|
||||
import { createCommandAliases, sendResponse } from "../utils/helpers.ts";
|
||||
import { Embed } from "../utils/Embed.ts";
|
||||
import { Args } from "../types/commands.ts";
|
||||
import { createCommand } from "./../../utils/helpers.ts";
|
||||
|
||||
botCache.commands.set(`kick`, {
|
||||
createCommand({
|
||||
name: `kick`,
|
||||
aliases: ["boot", "tempban"],
|
||||
description: "Kick command.",
|
||||
// adds cooldowns to the command
|
||||
cooldown: {
|
||||
@@ -369,7 +375,7 @@ botCache.commands.set(`kick`, {
|
||||
name: "member",
|
||||
type: "member",
|
||||
missing: function (message) {
|
||||
sendResponse(message, `User cannot be found.`);
|
||||
message.sendResponse(`User cannot be found.`);
|
||||
},
|
||||
// By default this is true but for the purpose of the guide so you can see this exists.
|
||||
required: true,
|
||||
@@ -383,40 +389,36 @@ botCache.commands.set(`kick`, {
|
||||
lowercase: true,
|
||||
},
|
||||
],
|
||||
execute: function (message, args: KickArgs, guild) {
|
||||
if (!guild) return;
|
||||
execute: function (message, args: KickArgs) {
|
||||
// setting up the embed for report/log
|
||||
const embed = new Embed()
|
||||
.setDescription(`Report: ${args.member.mention} Kick`)
|
||||
.addField("Reason >", args.reason)
|
||||
.addField("Time", message.timestamp.toString());
|
||||
|
||||
const reportchannel = guild.channels.find((channel) =>
|
||||
const reportchannel = message.guild?.channels.find((channel) =>
|
||||
channel.name === "report"
|
||||
);
|
||||
if (!reportchannel) {
|
||||
return sendResponse(message, "*`Report channel cannot be found!`*");
|
||||
return message.sendResponse("*`Report channel cannot be found!`*");
|
||||
}
|
||||
|
||||
// Delete the message command
|
||||
deleteMessage(message, "Remove kick command trigger.");
|
||||
message.delete("Remove kick command trigger.");
|
||||
// Kick the user with reason
|
||||
kick(guild, args.member.user.id, args.reason);
|
||||
args.member.kick(message.guildID, args.reason);
|
||||
// sends the kick report into log/report
|
||||
sendMessage(reportchannel, embed);
|
||||
reporchannel.send({ embed });
|
||||
},
|
||||
});
|
||||
|
||||
// other ways to call the command, must be in lowercase
|
||||
createCommandAliases("kick", ["boot", "tempban"]);
|
||||
|
||||
interface KickArgs {
|
||||
member: Member;
|
||||
reason: string;
|
||||
}
|
||||
```
|
||||
|
||||
Let's take a minute and explain the differences here. The first thing you will probably notice is different is the `arguments` property. Discordeno provides the `arguments` property because it provides argument handling/parsing/validating internally. You don't need to be splitting the message content or going through and validating it yourself. All you do is tell Discordeno that you want a member and a reason. It will do the magic and hard work to get you that data before you even run the command. You just do `args.member` and you have access to the full member object. You can also see that aliases are created slightly different but it's not that huge a impact. The end functionality is the same. There are a lot more powerful aspects to Discordeno like arguments. Keep diving in and you will find all the wonderful tools available to give you the best developer experience possible.
|
||||
Let's take a minute and explain the differences here. The first thing you will probably notice is different is the `arguments` property. Discordeno provides the `arguments` property because it provides argument handling/parsing/validating internally. You don't need to be splitting the message content or going through and validating it yourself. All you do is tell Discordeno that you want a member and a reason. It will do the magic and hard work to get you that data before you even run the command. You just do `args.member` and you have access to the full member object. There are a lot more powerful aspects to Discordeno like arguments. Keep diving in and you will find all the wonderful tools available to give you the best developer experience possible.
|
||||
|
||||
### Need More Examples/Help
|
||||
|
||||
|
||||
@@ -4,41 +4,13 @@
|
||||
|
||||
Discordeno provides first class support for TypeScript! Since Deno provides support for TypeScript, that also comes into Discordeno. This means you don't need to compile TypeScript before you use it. However, this isn't really why Discordeno is the best library for TypeScript developers. When I developed this library, I was experimenting with a lot of different things and one of them was automated typings.
|
||||
|
||||
Whenever I used other libraries, I was always seeing typings being inaccurate or problematic. This is because in any Discord API library, the majority is not used by the library itself so TypeScript doesn't warn the library developers. This makes it extremely likely that those typings become inaccurate or out of date because of simple mistakes like forgetting to update typings. Sometimes libraries will add a property and forget to add that on their typings. This makes it usable for JavaScript developers but not for TypeScript devs. For TypeScript developers, typings are everything! So I asked myself how could I solve this in my own library because I didn't want to have to suffer these problems again. The best solution was to not have any typings for the module at all.
|
||||
|
||||
In Discordeno, there are no typings created/maintained manually. It is all done **automatically** by TypeScript because of the design decisions of the code itself. **When the code is changed, the typings are automatically updated.** Never again will you suffer the problems of other libraries forgetting to keep their typings up to date properly.
|
||||
|
||||
## If Discordeno Doesn't Have Typings, What Is The Types Folder?
|
||||
|
||||
The types folder is typings built for Discord API Payload not for this lib. Discordeno provides these typings to provide the best developer experience possible when you code.
|
||||
Whenever I used other libraries, I was always seeing typings being inaccurate or problematic. This is because in any Discord API library, the majority is not used by the library itself so TypeScript doesn't warn the library developers. This makes it extremely likely that those typings become inaccurate or out of date because of simple mistakes like forgetting to update typings. Sometimes libraries will add a property and forget to add that on their typings. This makes it usable for JavaScript developers but not for TypeScript devs. For TypeScript developers, typings are everything! Discordeno treats typings as part of it's code! A breaking change in typings is a breaking change for the library!
|
||||
|
||||
## How Stable Is Discordeno?
|
||||
|
||||
One of the biggest issues with almost every library(I have used) is stability. None of the libraries gave much love and attention to TypeScript developers the way it deserves. Sometimes TypeScript projects would break because breaking changes to typings did not make a MAJOR bump so TypeScript bots in production would break. Sometimes I was personally maintaing the typings because no one else was for that lib. Some libs were pre 1.0 and didn't even have a stable branch/version where I would not have to worry about breaking changes.
|
||||
|
||||
This is why I made it one of my foundational goals of this library to have the best stability for TypeScript developers. No matter how small, a breaking change is a breaking change when it affects the public API. I could care less if we end up at version 500. Being afraid to bump a MAJOR because it's a small change or a typing change is a terrible decision as a library maintainer and destroys the experience for end users. Discordeno provides 2 separate versioning systems to provide you as much flexibility and stability as you like.
|
||||
|
||||
## What Do You Mean By 2 Separate Versioning Systems?
|
||||
|
||||
Discordeno will have releases that comply with SemVer. To use this system you will simply use the `v2.0.0` system in your version.
|
||||
|
||||
::: tip
|
||||
This means for every tiny bug fix/change you need to manually update the code every time. So if a new feature is added, you would need to bump the version in your code.
|
||||
:::
|
||||
|
||||
Each version is also available through a specific branch. For example v2 branch holds all the version 2 code. This branch is always updated whenever a MINOR or PATCH update is made that will NOT break your bots.
|
||||
|
||||
::: tip
|
||||
This means you never have to update your code EXCEPT when you are ready to bump to next MAJOR version. So if a new feature is added, it will be added automatically. If a small bug is fixed it will be automatic.
|
||||
:::
|
||||
|
||||
SemVer means more manual work for you to update code but a more secure module. Automated means almost no manual work for you to update code but a less secure module.
|
||||
|
||||
To understand that, SemVer makes it so you are using specific Release versions. In your code, you would do this by targeting the `..../discordeno/discordeno/v4.0.0/...` in order to use it. Whenever I make a small bug fix or new feature that does not break your code it would be released in a new release such as today's release of v4.0.1. This means you have to manually update your code to get these latest improvements. Until you do, you may have bugs or possibly missing features. The good part about SemVer is that if I make a mistake that could potentially make the code worse, it's a lot easier to move back to a proper version with SemVer.
|
||||
|
||||
The automated version would just simply be installed as soon as you reloaded cache for deno because it uses the branch itself as its url `.../discordeno/discordeno/v4/...` For example, when i start bots I use a script that reloads cache and restarts the bot making it so i am always using the latest code. Deno makes this possible because it can pull the latest code from any URL even github. So using Github branches to it's peak I create a branch for each version. These versions simply update automatically and you dont have to worry about updating. The only time you need to update is when you bump a MAJOR version like from v4 to v5. Because these may need you to make changes in your code. Note, even the good part about SemVer can be slightly removed by just locking a certain commit as well using this method.
|
||||
|
||||
At the end of the day, I think both systems can work and I am curious how everyone feels about them. I will be trying my best to maintain both systems.
|
||||
This is why I made it one of my foundational goals of this library to have the best stability for TypeScript developers. No matter how small, a breaking change is a breaking change when it affects the public API. I could care less if we end up at version 500. Being afraid to bump a MAJOR because it's a small change or a typing change is a terrible decision as a library maintainer and destroys the experience for end users.
|
||||
|
||||
## Why Doesn't Discordeno Use Classes or EventEmitter?
|
||||
|
||||
@@ -55,7 +27,7 @@ EventEmitter.emit('guildCreate', guild);
|
||||
// Discordeno Example
|
||||
eventHandlers.guildCreate?.(guild);
|
||||
```
|
||||
There isn't really any difference especially for users when they use it. One bad thing about EventEmitter is that if misused it can cause memory leaks. It is very easy to open yourself up to these memory leak issues. It has happened to me when I started coding as well. This is why I wanted Discordeno's implementation to help devs avoid the issues I had. It prevents anyone from having this as a potential issue. Another issue with EventEmitter is trying to update the code in those functions without having to deal with headaches left and right of removing and adding listeners.
|
||||
There isn't really any difference especially for users when they use it. One bad thing about EventEmitter is that if misused it can easily cause memory leaks. It is very easy to open yourself up to these memory leak issues. It has happened to me when I started coding as well. This is why I wanted Discordeno's implementation to help devs avoid the issues I had. It prevents anyone from having this as a potential issue. Another issue with EventEmitter is trying to update the code in those functions without having to deal with headaches left and right of removing and adding listeners. You don't need to worry about binding or not binding events. They are just pure functions
|
||||
|
||||
In Discordeno, this is extremely simple, you just simply give it the new event handlers.
|
||||
|
||||
@@ -80,8 +52,7 @@ Discordeno is the only library(that I have used), that has built in permission h
|
||||
Discordeno provides you specific keywords that you can use to send a clean response to the end user of your choosing. I have even seen some bots have hundreds of thousands of Missing Permission or Missing Access errors because libraries don't handle it. IMO, this is a crucial part of any good library as much as it is to handle rate limiting.
|
||||
|
||||
```typescript
|
||||
import { Errors } from "https://raw.githubusercontent.com/discordeno/discordeno/v4/types/errors.ts";
|
||||
import { Message } from "https://raw.githubusercontent.com/discordeno/discordeno/v4/structures/message.ts";
|
||||
import { Errors, Message } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
|
||||
|
||||
export function handleCommandError(message: Message, type: Errors) {
|
||||
switch (type) {
|
||||
|
||||
@@ -32,7 +32,7 @@ Now you've created an Application but it will need some code in order for it to
|
||||
|
||||
You can install Discordeno by importing:
|
||||
```ts
|
||||
import Client from "https://deno.land/x/discordeno@9.4.0/src/module/client.ts";
|
||||
import { startBot } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
@@ -40,10 +40,10 @@ import Client from "https://deno.land/x/discordeno@9.4.0/src/module/client.ts";
|
||||
Starting with Discordeno is very simple, you can start from scratch without any boilerplates/frameworks: Add this snippet of code into a new TypeScript file:
|
||||
|
||||
```ts
|
||||
import StartBot, { sendMessage, Intents } from "https://deno.land/x/discordeno@9.4.0/src/module/client.ts";
|
||||
import { startBot, Intents } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
|
||||
import config from "./config.ts";
|
||||
|
||||
StartBot({
|
||||
startBot({
|
||||
token: config.token,
|
||||
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
|
||||
eventHandlers: {
|
||||
@@ -52,7 +52,7 @@ StartBot({
|
||||
},
|
||||
messageCreate: (message) => {
|
||||
if (message.content === "!ping") {
|
||||
sendMessage(message.channelID, "Pong");
|
||||
message.reply("Pong");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
docs/src/releasenotes.md
Normal file
96
docs/src/releasenotes.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Discordeno v10 Release Notes
|
||||
|
||||
From breaking changes to small bug fixes to QoL improvements, this release has it all. This is Discordeno Next!
|
||||
|
||||
## Slash Commands and Interactions
|
||||
|
||||
A Slash Command is a command that you register for your application. It consists of a name, description, and a block of options, which you can think of like arguments to a function.
|
||||
|
||||
An Interaction is the message that your application receives when a user uses a command.
|
||||
|
||||
Discordeno, from v10 or newer, has inbuilt support for both slash commands and interactions. For a quick start, check out our [official boilerplate for slash commands](https://github.com/discordeno/discordeno-slashbot-boilerplate).
|
||||
|
||||

|
||||
|
||||
## Getters for Structures
|
||||
|
||||
Getters are introduced in Discordeno v10 as a QoL improvement.
|
||||
|
||||
- v9 or before
|
||||
|
||||
```ts
|
||||
import { sendMessage } from "discordeno";
|
||||
|
||||
await sendMessage("Channel ID", "Content");
|
||||
...
|
||||
```
|
||||
|
||||
- v10 or newer
|
||||
|
||||
```ts
|
||||
message.send("Content");
|
||||
```
|
||||
|
||||
## Permission enums to Permission strings
|
||||
|
||||
This change is a breaking change, it increases consistency by enabling users to use consistent API throughout their code.
|
||||
|
||||
- v9 or before:
|
||||
|
||||
```ts
|
||||
import { hasChannelPermissions, Permissions } from "discordeno";
|
||||
|
||||
await hasChannelPermissions(
|
||||
"Channel ID",
|
||||
"User ID",
|
||||
[Permissions.MANAGE_MESSAGES]
|
||||
);
|
||||
```
|
||||
|
||||
- v10 or newer:
|
||||
|
||||
```ts
|
||||
import { hasChannelPermissions } from "discordeno";
|
||||
|
||||
await hasChannelPermissions("Channel ID", "User ID", ["MANAGE_MESSAGES"]);
|
||||
```
|
||||
|
||||
## Guild.channels and Guild.members as Getters
|
||||
|
||||
These changes were made to optimize memory. To properly explain these changes, let's take an example of a user in X number of guilds, previously, in v9 or before, the member object was cached X times. However, with the release of v10, it stores the member object only once, hence saving memory.
|
||||
|
||||
Similarly, `Guild.channels` is converted into a getter in v10 or newer. This is removed because it was storing duplicated values from `cache.channels`, and hence taking additional memory.
|
||||
|
||||
## Improved WebSocket Close Errors
|
||||
|
||||
- v9 or before:
|
||||
|
||||
```ts
|
||||
import { createClient } from "discordeno";
|
||||
|
||||
createClient({
|
||||
token: "BOT TOKEN",
|
||||
// Invalid intents
|
||||
intents: [1234567890]
|
||||
});
|
||||
|
||||
// throws: "Shard.ts: Error occurred that is not resumeable or able to be reconnected."
|
||||
```
|
||||
|
||||
- v10 or later:
|
||||
|
||||
```ts
|
||||
import { startBot } from "discordeno";
|
||||
|
||||
startBot({
|
||||
token: "BOT TOKEN",
|
||||
intents: [1234567890]
|
||||
});
|
||||
|
||||
// throws: "[Invalid intent(s)] Sent an invalid intent for a Gateway Intent."
|
||||
```
|
||||
|
||||
If you are already using Discordeno, you can update to `v10` by updating the import URL for Discordeno to the following, usually present in `deps.ts` file.
|
||||
```
|
||||
https://deno.land/x/discordeno@10.0.0/mod.ts
|
||||
```
|
||||
@@ -6,10 +6,9 @@ Discordeno will help make Discord bot development much easier. Don't worry, as y
|
||||
|
||||
> This guide is going to assume you already have the basic requirements to make a bot ready. This includes github, git, a code editor like Visual Studio Code. If you don't have these yet please prepare them first before going forward.
|
||||
|
||||
- First, create a Discordeno Bot using the [Generator Boilerplate](https://github.com/Skillz4Killz/Discordeno-bot-template/generate). Give it any name you like. For the purpose of this guide we will call it, Stargate.
|
||||
- First, create a Discordeno Bot using the [Generator Boilerplate](https://github.com/discordeno/discordeno-bot-template/generate). Give it any name you like. For the purpose of this guide we will call it, Stargate.
|
||||
|
||||
- Then `git clone https://github.com/Skillz4Killz/Stargate.git` Replace **Stargate** with the name you chose.
|
||||
|
||||
- When that is done, go ahead and open up the folder with VSC.
|
||||
- Create a new file called `configs.ts`. Open the `configs.example.ts` file and copy everything over.
|
||||
|
||||
@@ -109,9 +108,13 @@ Oh my god! You now have a bot with a bunch of features already! You don't believ
|
||||
3. Run the script below:
|
||||
|
||||
```shell
|
||||
deno run --allow-net --allow-read --no-check --config tsconfig.json mod.ts
|
||||
deno run -A --no-check mod.ts
|
||||
```
|
||||
|
||||
> The `-A` flag will grant it all permissions to run the bot. If you would like to specify specific ones you may do so!
|
||||
|
||||
> The `--no-check` flag is used for module augmentation support as Deno still does not provide a clean way to have it. If you don't use custom structures, this is not needed.
|
||||
|
||||
The first time you run it, you may see a lot of files being loaded. This is preparing all the magic behind the scene. Once it is ready, you will see something like this:
|
||||
|
||||

|
||||
|
||||
@@ -7,24 +7,21 @@ Really great job. Now, lets dive into trying to use some of the commands and try
|
||||
Let's first start by taking an existing command and slightly modifying it to your needs. Let's use the `Invite` command as our example. When you open the command, you will see something like this:
|
||||
|
||||
```ts
|
||||
import { botCache } from "../../mod.ts";
|
||||
import { sendMessage } from "https://deno.land/x/discordeno@9.4.0/src/handlers/channel.ts";
|
||||
import { botID } from "https://deno.land/x/discordeno@9.4.0/src/module/client.ts";
|
||||
import { botCache } from "../../deps.ts";
|
||||
import { createCommand } from "../utils/helpers.ts"
|
||||
|
||||
createCommand({
|
||||
name: "invite",
|
||||
execute: function (message) {
|
||||
// Replace the permission number at the end to request the permissions you wish to request. By default, this will request Admin perms.
|
||||
sendMessage(
|
||||
message.channelID,
|
||||
message.reply(
|
||||
`https://discordapp.com/oauth2/authorize?client_id=${botID}&scope=bot&permissions=8`,
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Let's break this down. The first four lines are importing the necessary things from their files so we can use them in this command. Don't worry if this doesn't make sense, most of the time, this will all be done automatically for you if you use a good code editor like Visual Studio Code. We create a command by doing:
|
||||
Let's break this down. The first two lines are importing the necessary things from their files so we can use them in this command. Don't worry if this doesn't make sense, most of the time, this will all be done automatically for you if you use a good code editor like Visual Studio Code. We create a command by doing:
|
||||
|
||||
```ts
|
||||
createCommand({
|
||||
@@ -61,7 +58,7 @@ description: "Like the bot? Use this link to add it to your server!",
|
||||
🎉 It's that simple. So let's restart the bot and see how it changed. Use **CTRL + C** to shut down the bot. Then run the command from earlier.
|
||||
|
||||
```shell
|
||||
deno run --allow-net --allow-read --no-check --config tsconfig.json mod.ts
|
||||
deno run -A --no-check mod.ts
|
||||
```
|
||||
|
||||
To access this easily, most likely all you need to do is press the **UP ARROW** key. Feel free to copy paste this if it doesn't work.
|
||||
@@ -80,8 +77,6 @@ aliases: ["inv", "join"],
|
||||
description: "Like the bot? Use this link to add it to your server!",
|
||||
```
|
||||
|
||||
> **Note:** If you only want to add 1 alias, you can pass in a simple string instead of an array as well as the second argument.
|
||||
|
||||
Notice, I added 2 aliases here. You can add as many aliases as you like. If you see a lot of users typing it wrong by accident you can add those typos as aliases as well.
|
||||
|
||||
Let's start testing this out. But first, let's understand something important.
|
||||
@@ -113,10 +108,9 @@ If you get stuck, don't worry. When you are ready, let's continue to the next st
|
||||
Let's make a command that will allow guild admins to give or take roles from a member. Since, we are creating a `Moderation` command to give or take roles, let's go ahead and create a category folder called `Moderation` and then create a file called `role.ts`. Once the file is made, you can paste this following base snippet to make our first command.
|
||||
|
||||
```ts
|
||||
import { botCache } from "../../mod.ts";
|
||||
import { sendMessage } from "https://deno.land/x/discordeno@9.4.0/src/handlers/channel.ts";
|
||||
import { botCache } from "../../deps.ts";
|
||||
import { PermissionLevels } from "../types/commands.ts";
|
||||
import { createCommandAliases } from "../utils/helpers";
|
||||
import { createCommand } from "../utils/helpers.ts";
|
||||
|
||||
createCommand({
|
||||
name: "commandname",
|
||||
@@ -139,8 +133,6 @@ createCommand({
|
||||
// The code for your command goes here
|
||||
}
|
||||
});
|
||||
|
||||
// createCommandAliases("commandname", ["alias"])
|
||||
```
|
||||
|
||||
## Understanding Command Options
|
||||
@@ -151,8 +143,6 @@ Woah! We just added a massive file but most of that stuff is new to us. Don't wo
|
||||
|
||||
Before we start, quickly update the command name and description.
|
||||
|
||||
> **Note:** Once you highlight the `commandname`, press **CTRL + SHIFT + L** to select ALL the `commandname` on the file and you can easily replace them at once. Another useful shortcut you may use when coding your bot is **CTRL + D** after highlighting. This shortcut selects the next place where the same text exists as thing in the file that has the same thing as you highlighted.
|
||||
|
||||
## dmOnly & guildOnly Options
|
||||
|
||||
The `dmOnly` option is available if you want to make sure this command only runs in a direct message. If this command is ran in a server, the command will immediately exit out.
|
||||
@@ -169,7 +159,9 @@ For the purpose of this guide, we want our `role` comamnd to only be run in a se
|
||||
|
||||
NSFW stands for **Not Safe For Work**. One of Discord's rules is that you enforce that NSFW content is sent only in NSFW channels. Discordeno has this built in. You simply tell Discordeno, that you want a command to be considered `nsfw` or not.
|
||||
|
||||
If this option is enabled, this command will only be able to be used in a `nsfw` channel on a server. Discord does not consider Direct Messages as nsfw safe!
|
||||
If this option is enabled, this command will only be able to be used in a `nsfw` channel on a server.
|
||||
|
||||
> Discord does not consider Direct Messages as nsfw safe!
|
||||
|
||||
## Permission Level Option
|
||||
|
||||
@@ -250,14 +242,14 @@ arguments: [
|
||||
name: "member",
|
||||
type: "member",
|
||||
missing: (message) => {
|
||||
sendResponse(message, `you did not provide a member to give the role to. You can provide a @member mention, a member ID, or try using their nickname/username. The nickname/username will only work if they have been active in your server recently.`)
|
||||
message.sendResponse(`You did not provide a member to give the role to. You can provide a @member mention, a member ID, or try using their nickname/username. The nickname/username will only work if they have been active in your server recently.`)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "role",
|
||||
missing: (message) => {
|
||||
sendResponse(message, `you did not provide a role to give. You can provide a @role mention, a role ID, or it's name. If the role name did not work, try to use the **roleinfo** command to get the role ID.`)
|
||||
message.sendResponse(`You did not provide a role to give. You can provide a @role mention, a role ID, or it's name. If the role name did not work, try to use the **roleinfo** command to get the role ID.`)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -320,7 +312,7 @@ To do this, we are going to want something like this:
|
||||
|
||||
```ts
|
||||
if (args.role.id === message.guildID) {
|
||||
return sendResponse(message, "The everyone role can not be given to anyone because everyone has the everyone role already. *Keep calm and let Carter figure it out*!");
|
||||
return message.sendResponse("The everyone role can not be given to anyone because everyone has the everyone role already. *Keep calm and let Carter figure it out*!");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -334,7 +326,7 @@ import { Role, Member } from "../../deps.ts";
|
||||
execute: function (message, args: RoleArgs, guild) {
|
||||
// If this was the everyone role alert with a silly error
|
||||
if (args.role.id === message.guildID) {
|
||||
return sendResponse(message, "Are you trying to make this person a super hero? Everyone has the everyone role. I can't give the everyone role to another user.");
|
||||
return message.sendResponse("Are you trying to make this person a super hero? Everyone has the everyone role. I can't give the everyone role to another user.");
|
||||
}
|
||||
|
||||
// Lots of comments here hidden so you can see the changes easily.
|
||||
@@ -355,55 +347,54 @@ Awesome! Let's keep going.
|
||||
execute: async function (message, args: RoleArgs, guild) {
|
||||
// If this was the everyone role alert with a silly error
|
||||
if (args.role.id === message.guildID) {
|
||||
return sendResponse(message, "I don't know if you noticed or not but I'm an extremely arrogant bot who tends to think all of his plans will work. But I can't give the everyone role to someone.");
|
||||
return message.sendResponse("I don't know if you noticed or not but I'm an extremely arrogant bot who tends to think all of his plans will work. But I can't give the everyone role to someone.");
|
||||
}
|
||||
|
||||
// If this is a managed role(some bots role) we can't give/remove alert with silly error
|
||||
if (args.role.managed) {
|
||||
return sendResponse(message, "Dammit man, just 'cause I'm Scottish doesn't mean I can give your people managed roles.")
|
||||
return message.sendResponse("Dammit man, just 'cause I'm Scottish doesn't mean I can give your people managed roles.")
|
||||
}
|
||||
|
||||
// Get the bots highest role
|
||||
const botsHighestRole = highestRole(message.guildID, botID);
|
||||
const botsHighestRole = await highestRole(message.guildID, botID);
|
||||
|
||||
// Check if the bot has a role higher than the role that it will try to give.
|
||||
const botIsHigher = higherRolePosition(message.guildID, botsHighestRole.id, args.role.id)
|
||||
const botIsHigher = await higherRolePosition(message.guildID, botsHighestRole.id, args.role.id)
|
||||
|
||||
// If the role is too high alert the user.
|
||||
if (!botIsHigher) {
|
||||
return sendResponse(message, "Okay look, asking me give a role that is higher than my highest role is ridiculous! I am the first bot to admit I don't know who these people are nor do I care to. Look, if you'd like I could take you down the hall and just point at the people who annoy me more than the rest. But that's about as useful as I get.")
|
||||
return message.sendResponse("Okay look, asking me give a role that is higher than my highest role is ridiculous! I am the first bot to admit I don't know who these people are nor do I care to. Look, if you'd like I could take you down the hall and just point at the people who annoy me more than the rest. But that's about as useful as I get.")
|
||||
}
|
||||
|
||||
// Check the command author's highest role
|
||||
const membersHighestRole = highestRole(message.guildID, message.author.id);
|
||||
const membersHighestRole = await highestRole(message.guildID, message.author.id);
|
||||
|
||||
// If the author does not have a role high enough to give this role alert
|
||||
if (!higherRolePosition(message.guildID, membersHighestRole.id, args.role.id)) {
|
||||
return sendResponse(message, "In my culture, whenever someone tries to give a role that is higher than their highest role, I would be well within my rights to dismember you.")
|
||||
if (!(await higherRolePosition(message.guildID, membersHighestRole.id, args.role.id))) {
|
||||
return message.sendResponse("In my culture, whenever someone tries to give a role that is higher than their highest role, I would be well within my rights to dismember you.")
|
||||
}
|
||||
|
||||
// If the user has this role already we should remove it
|
||||
if (message.member().roles.includes(args.role.id)) {
|
||||
removeRole(message.guildID, args.member.user.id, args.role.id, `${message.author.tag} used the role command to remove this role.`)
|
||||
if (message.member?.guilds.get(message.guildID)?.roles.includes(args.role.id)) {
|
||||
message.member.removeRole(message.guildID, args.role.id, `${message.author.tag} used the role command to remove this role.`)
|
||||
// Alert the user that used the command that the user has lost the role.
|
||||
return sendResponse(message, `The role **${args.role.name}** has been removed from **${args.member.tag}**.`)
|
||||
return message.sendResponse(`The role **${args.role.name}** has been removed from **${args.member.tag}**.`)
|
||||
}
|
||||
|
||||
// Add the role to the user.
|
||||
addRole(guildID, memberID, roleID)
|
||||
message.member?.addRole(guildID, roleID)
|
||||
|
||||
// Alert the user that used the command that the user has been give the role.
|
||||
return sendResponse(message, `The role **${args.role.name}** has been added to **${args.member.tag}**.`)
|
||||
return message.sendResponse(`The role **${args.role.name}** has been added to **${args.member.tag}**.`)
|
||||
}
|
||||
```
|
||||
> **Note:** Asynchronous functions are an advanced topic, and you do not need to worry about them now. Basically, we need to do that because checking the highest Role requires the use of await.
|
||||
|
||||
The final version of the command should look something like this:
|
||||
|
||||
```ts
|
||||
import { botCache, highestRole, higherRolePosition, botID, removeRole, addRole, Role, Member } from "../../deps.ts";
|
||||
import { botCache, highestRole, higherRolePosition, botID, Role, Member } from "../../deps.ts";
|
||||
import { PermissionLevels } from "../types/commands.ts";
|
||||
import { createCommand, sendResponse } from "../utils/helpers.ts";
|
||||
import { createCommand } from "../utils/helpers.ts";
|
||||
|
||||
createCommand({
|
||||
name: "role",
|
||||
@@ -419,30 +410,28 @@ createCommand({
|
||||
name: "member",
|
||||
type: "member",
|
||||
missing: (message) => {
|
||||
sendResponse(message, `you did not provide a member to give the role to. You can provide a @member mention, a member ID, or try using their nickname/username. The nickname/username will only work if they have been active in your server recently.`)
|
||||
message.sendResponse(`You did not provide a member to give the role to. You can provide a @member mention, a member ID, or try using their nickname/username. The nickname/username will only work if they have been active in your server recently.`)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "role",
|
||||
missing: (message) => {
|
||||
sendResponse(message, `you did not provide a role to give. You can provide a @role mention, a role ID, or it's name. If the role name did not work, try to use the **roleinfo** command to get the role ID.`)
|
||||
message.sendResponse(`You did not provide a role to give. You can provide a @role mention, a role ID, or it's name. If the role name did not work, try to use the **roleinfo** command to get the role ID.`)
|
||||
}
|
||||
}
|
||||
],
|
||||
execute: async function (message, args: RoleArgs) {
|
||||
// If this was the everyone role alert with a silly error
|
||||
if (args.role.id === message.guildID) {
|
||||
return sendResponse(
|
||||
message,
|
||||
return message.sendResponse(
|
||||
"I don't know if you noticed or not but I'm an extremely arrogant bot who tends to think all of his plans will work. But I can't give the everyone role to someone.",
|
||||
);
|
||||
}
|
||||
|
||||
// If this is a managed role(some bots role) we can't give/remove alert with silly error
|
||||
if (args.role.managed) {
|
||||
return sendResponse(
|
||||
message,
|
||||
return message.sendResponse(
|
||||
"Dammit man, just 'cause I'm Scottish doesn't mean I can give your people managed roles.",
|
||||
);
|
||||
}
|
||||
@@ -451,13 +440,12 @@ createCommand({
|
||||
const botsHighestRole = await highestRole(message.guildID, botID);
|
||||
|
||||
// Check if the bot has a role higher than the role that it will try to give. If the role is too high alert the user.
|
||||
if (!botsHighestRole || !higherRolePosition(
|
||||
if (!botsHighestRole || !(await higherRolePosition(
|
||||
message.guildID,
|
||||
botsHighestRole.id,
|
||||
args.role.id,
|
||||
)) {
|
||||
return sendResponse(
|
||||
message,
|
||||
))) {
|
||||
return message.sendResponse(
|
||||
"Okay look, asking me give a role that is higher than my highest role is ridiculous! I am the first bot to admit I don't know who these people are nor do I care to. Look, if you'd like I could take you down the hall and just point at the people who annoy me more than the rest. But that's about as useful as I get.",
|
||||
);
|
||||
}
|
||||
@@ -467,35 +455,31 @@ createCommand({
|
||||
|
||||
// If the author does not have a role high enough to give this role alert
|
||||
if (!membersHighestRole ||
|
||||
!higherRolePosition(message.guildID, membersHighestRole.id, args.role.id)
|
||||
!(await higherRolePosition(message.guildID, membersHighestRole.id, args.role.id))
|
||||
) {
|
||||
return sendResponse(
|
||||
message,
|
||||
return message.sendResponse(
|
||||
"In my culture, whenever someone tries to give a role that is higher than their highest role, I would be well within my rights to dismember you.",
|
||||
);
|
||||
}
|
||||
|
||||
// If the user has this role already we should remove it
|
||||
if (message.member?.roles.includes(args.role.id)) {
|
||||
removeRole(
|
||||
if (message.member?.guilds.get(message.guildID)?.roles.includes(args.role.id)) {
|
||||
message.member.removeRole(
|
||||
message.guildID,
|
||||
args.member.user.id,
|
||||
args.role.id,
|
||||
`${message.author.username} used the role command to remove this role.`,
|
||||
);
|
||||
// Alert the user that used the command that the user has lost the role.
|
||||
return sendResponse(
|
||||
message,
|
||||
return message.sendResponse(
|
||||
`The role **${args.role.name}** has been removed from **${args.member.tag}**.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Add the role to the user.
|
||||
addRole(message.guildID, args.member.user.id, args.role.id, `${message.author.username} used the role command to give this role.`);
|
||||
message.member?.addRole(message.guildID, args.role.id, `${message.author.username} used the role command to give this role.`);
|
||||
|
||||
// Alert the user that used the command that the user has been give the role.
|
||||
return sendResponse(
|
||||
message,
|
||||
return message.sendResponse(
|
||||
`The role **${args.role.name}** has been added to **${args.member.tag}**.`,
|
||||
);
|
||||
},
|
||||
@@ -548,6 +532,7 @@ const funCommandData = [
|
||||
funCommandData.forEach((data) => {
|
||||
botCache.commands.set({
|
||||
name: data.name,
|
||||
aliases: data.aliases,
|
||||
botChannelPermissions: ["SEND_MESSAGES", "EMBED_LINKS"],
|
||||
cooldown: {
|
||||
seconds: 2,
|
||||
@@ -585,10 +570,6 @@ funCommandData.forEach((data) => {
|
||||
return sendEmbed(message.channelID, embed);
|
||||
},
|
||||
});
|
||||
|
||||
if (data.aliases?.length) {
|
||||
createCommandAliases(data.name, data.aliases);
|
||||
}
|
||||
});
|
||||
|
||||
interface FunArgs {
|
||||
@@ -596,9 +577,7 @@ interface FunArgs {
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** The imports in this are a bit different. I did this to show you that you can also import everything grouped like this through the `deps.ts` which will make everything available to you at ease.
|
||||
|
||||
> **Note:** This is only an example of dynamic command creation and won't work if you try using this code. Since this is an advanced topic we're not going to cover this in more detail here, because we will have an entire in depth guide for dynamic command creation. If you want to pause and learn it now, feel free: [Dynamic Command Creation Advanced Guide](https://discordeno.mod.land/advanced/dynamiccommands.html)
|
||||
> **Note:** This is only an example of dynamic command creation and won't work if you try using this code since you won't have the configs necessary. Since this is an advanced topic we're not going to cover this in more detail here, because we will have an entire in depth guide for dynamic command creation. If you want to pause and learn it now, feel free: [Dynamic Command Creation Advanced Guide](https://discordeno.mod.land/advanced/dynamiccommands.html)
|
||||
|
||||
Take a minute to realize what just happened. This has made 18 different unique commands dynamically. In 1 file, using the same piece of code, we created so many commands. You can easily add more commands to this. For example, if you wanted to add weeb (animated) versions of these commands. Then you are at 36 commands with 1 simple command file.
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ An event in Discordeno is a function that can be called when a specific event oc
|
||||
Go ahead and open up the `src/events/ready.ts` file. When you open this file, you will see the code that is triggered on the `ready` event. Whenever the bot completely starts up, Discordeno emits the `ready` event. This is when this code will be run allowing you to log these messages.
|
||||
|
||||
```ts
|
||||
import { botCache } from "../../mod.ts";
|
||||
import { cache } from "https://deno.land/x/discordeno@9.4.0/src/utils/cache.ts";
|
||||
import { botCache, cache } from "../../deps.ts";
|
||||
|
||||
botCache.eventHandlers.ready = function () {
|
||||
console.log(`Loaded ${botCache.arguments.size} Argument(s)`);
|
||||
@@ -71,10 +70,8 @@ botCache.eventHandlers.discordLog = function (error) {
|
||||
].join("\n"))
|
||||
.setTimestamp();
|
||||
|
||||
// Get the channel we need to send this error to
|
||||
const errorChannel = configs.channelIDs.errorChannelID;
|
||||
// If the channel is not found cancel out
|
||||
if (!errorChannel) return;
|
||||
if (!configs.channelIDs.errorChannelID) return;
|
||||
|
||||
// Send the message
|
||||
return sendEmbed(errorChannel, embed);
|
||||
@@ -90,8 +87,8 @@ Now that we have fully covered events, it would be a good time to get some pract
|
||||
channelDelete?: (channel: Channel) => unknown;
|
||||
debug?: (args: DebugArg) => unknown;
|
||||
dispatchRequirements?: (data: DiscordPayload, shardID: number) => unknown;
|
||||
guildBanAdd?: (guild: Guild, user: Member | UserPayload) => unknown;
|
||||
guildBanRemove?: (guild: Guild, user: Member | UserPayload) => unknown;
|
||||
guildBanAdd?: (guild: Guild, user: UserPayload, member?: Member) => unknown;
|
||||
guildBanRemove?: (guild: Guild, user: UserPayload, member?: Member) => unknown;
|
||||
guildCreate?: (guild: Guild) => unknown;
|
||||
guildLoaded?: (guild: Guild) => unknown;
|
||||
guildUpdate?: (guild: Guild, changes: GuildUpdateChange[]) => unknown;
|
||||
@@ -102,7 +99,7 @@ Now that we have fully covered events, it would be a good time to get some pract
|
||||
cachedEmojis: Emoji[],
|
||||
) => unknown;
|
||||
guildMemberAdd?: (guild: Guild, member: Member) => unknown;
|
||||
guildMemberRemove?: (guild: Guild, member: Member | UserPayload) => unknown;
|
||||
guildMemberRemove?: (guild: Guild, user: UserPayload, member?: Member) => unknown;
|
||||
guildMemberUpdate?: (
|
||||
guild: Guild,
|
||||
member: Member,
|
||||
@@ -110,7 +107,7 @@ Now that we have fully covered events, it would be a good time to get some pract
|
||||
) => unknown;
|
||||
heartbeat?: () => unknown;
|
||||
messageCreate?: (message: Message) => unknown;
|
||||
messageDelete?: (message: Message | PartialMessage) => unknown;
|
||||
messageDelete?: (partial: PartialMessage, message?: Message) => unknown;
|
||||
messageUpdate?: (message: Message, cachedMessage: OldMessage) => unknown;
|
||||
nicknameUpdate?: (
|
||||
guild: Guild,
|
||||
@@ -126,14 +123,16 @@ Now that we have fully covered events, it would be a good time to get some pract
|
||||
rawGateway?: (data: unknown) => unknown;
|
||||
ready?: () => unknown;
|
||||
reactionAdd?: (
|
||||
message: Message | MessageReactionPayload,
|
||||
payload: MessageReactionPayload
|
||||
emoji: ReactionPayload,
|
||||
userID: string,
|
||||
message?: Message,
|
||||
) => unknown;
|
||||
reactionRemove?: (
|
||||
message: Message | MessageReactionPayload,
|
||||
payload: MessageReactionPayload,
|
||||
emoji: ReactionPayload,
|
||||
userID: string,
|
||||
message?: Message,
|
||||
) => unknown;
|
||||
reactionRemoveAll?: (data: BaseMessageReactionPayload) => unknown;
|
||||
reactionRemoveEmoji?: (data: MessageReactionRemoveEmojiPayload) => unknown;
|
||||
|
||||
@@ -43,18 +43,18 @@ if (!command.vipOnly) return false;
|
||||
// The bot's support server
|
||||
const guild = cache.guilds.get(configs.supportServerID);
|
||||
// If the command author is not in the server they won't have the vip role
|
||||
const member = guild.members.get(message.author.id) || await getMember(guild.id, message.author.id);
|
||||
const member = message.member || await getMember(guild.id, message.author.id).catch(console.error);
|
||||
// Member doesn't exist so cancel the command
|
||||
if (!member) {
|
||||
sendResponse(message, `sorry, but you can not use this command until you become VIP. **Close the IRIS!!!**`)
|
||||
message.sendResponse(`Sorry, but you can not use this command until you become VIP. **Close the IRIS!!!**`)
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the user has the vip role on the support server given by patreon allow the command
|
||||
if (member.roles.includes(configs.roleIDs.patreonVIPRoleID)) return false;
|
||||
if (member.guilds.get(message.guildID)?.roles.includes(configs.roleIDs.patreonVIPRoleID)) return false;
|
||||
|
||||
// Alert the user they don't have vip and can't use the command
|
||||
sendResponse(message, `sorry, but you can not use this command until you become a VIP. I'm sorry, Teal'c. We'll go to Disneyland next year. I promise.`)
|
||||
message.sendResponse(`Sorry, but you can not use this command until you become a VIP. I'm sorry, Teal'c. We'll go to Disneyland next year. I promise.`)
|
||||
// Cancel the command since the user does not have vip
|
||||
return true;
|
||||
```
|
||||
|
||||
@@ -44,8 +44,8 @@ The monitor options are very similar in functionality with the command options.
|
||||
Similar to the command name, we will specify a unique name for the monitors. In this case let's call it inviteFilter
|
||||
|
||||
```ts
|
||||
botCache.monitors.set("monitorname", {
|
||||
name: "monitorname",
|
||||
botCache.monitors.set("inviteFilter", {
|
||||
name: "inviteFilter",
|
||||
```
|
||||
|
||||
### Ignore Options
|
||||
@@ -72,7 +72,6 @@ botChannelPermissions: ["MANAGE_MESSAGES"]
|
||||
```ts
|
||||
import { botCache, deleteMessage } from "../../deps.ts";
|
||||
import { translate } from "../utils/i18next.ts";
|
||||
import { sendAlertResponse } from "../utils/helpers.ts";
|
||||
|
||||
botCache.monitors.set("inviteFilter", {
|
||||
name: "inviteFilter",
|
||||
@@ -89,12 +88,11 @@ botCache.monitors.set("inviteFilter", {
|
||||
// This message has an invite link, so delete the message.
|
||||
try {
|
||||
// Delete the invite link
|
||||
deleteMessage(
|
||||
message,
|
||||
message.delete(
|
||||
translate(message.guildID, `monitors/invitefilter:DELETE_REASON`),
|
||||
);
|
||||
// Send a message to the user so they know why the message was deleted. Then delete the response after 5 seconds to prevent spam.
|
||||
sendAlertResponse(message, translate(message.guildID, "monitors/invitefilter:DELETE_ALERT_MESSAGE"), 5)
|
||||
message.alertResponse(translate(message.guildID, "monitors/invitefilter:DELETE_ALERT_MESSAGE"), 5)
|
||||
} catch (error) {
|
||||
return botCache.eventHandlers.discordLog(error)
|
||||
}
|
||||
|
||||
75
mod.ts
75
mod.ts
@@ -1,46 +1,29 @@
|
||||
import createClient from "./src/module/client.ts";
|
||||
|
||||
export * from "./src/controllers/bans.ts";
|
||||
export * from "./src/controllers/cache.ts";
|
||||
export * from "./src/controllers/channels.ts";
|
||||
export * from "./src/controllers/guilds.ts";
|
||||
export * from "./src/controllers/members.ts";
|
||||
export * from "./src/controllers/messages.ts";
|
||||
export * from "./src/controllers/misc.ts";
|
||||
export * from "./src/controllers/mod.ts";
|
||||
export * from "./src/controllers/reactions.ts";
|
||||
export * from "./src/controllers/roles.ts";
|
||||
export * from "./src/handlers/channel.ts";
|
||||
export * from "./src/handlers/guild.ts";
|
||||
export * from "./src/handlers/member.ts";
|
||||
export * from "./src/handlers/message.ts";
|
||||
export * from "./src/handlers/webhook.ts";
|
||||
export * from "./src/module/client.ts";
|
||||
export * from "./src/module/requestManager.ts";
|
||||
export * from "./src/module/shardingManager.ts";
|
||||
export * from "./src/structures/channel.ts";
|
||||
export * from "./src/structures/guild.ts";
|
||||
export * from "./src/structures/member.ts";
|
||||
export * from "./src/structures/message.ts";
|
||||
export * from "./src/structures/mod.ts";
|
||||
export * from "./src/structures/role.ts";
|
||||
export * from "./src/types/activity.ts";
|
||||
export * from "./src/types/cdn.ts";
|
||||
export * from "./src/types/channel.ts";
|
||||
export * from "./src/types/discord.ts";
|
||||
export * from "./src/types/errors.ts";
|
||||
export * from "./src/types/fetch.ts";
|
||||
export * from "./src/types/guild.ts";
|
||||
export * from "./src/types/member.ts";
|
||||
export * from "./src/types/message.ts";
|
||||
export * from "./src/types/options.ts";
|
||||
export * from "./src/types/permission.ts";
|
||||
export * from "./src/types/presence.ts";
|
||||
export * from "./src/types/role.ts";
|
||||
export * from "./src/utils/cache.ts";
|
||||
export * from "./src/utils/cdn.ts";
|
||||
export * from "./src/utils/collection.ts";
|
||||
export * from "./src/utils/permissions.ts";
|
||||
export * from "./src/utils/utils.ts";
|
||||
|
||||
export default createClient;
|
||||
export * from "./src/api/controllers/bans.ts";
|
||||
export * from "./src/api/controllers/cache.ts";
|
||||
export * from "./src/api/controllers/channels.ts";
|
||||
export * from "./src/api/controllers/guilds.ts";
|
||||
export * from "./src/api/controllers/members.ts";
|
||||
export * from "./src/api/controllers/messages.ts";
|
||||
export * from "./src/api/controllers/misc.ts";
|
||||
export * from "./src/api/controllers/mod.ts";
|
||||
export * from "./src/api/controllers/reactions.ts";
|
||||
export * from "./src/api/controllers/roles.ts";
|
||||
export * from "./src/api/handlers/channel.ts";
|
||||
export * from "./src/api/handlers/guild.ts";
|
||||
export * from "./src/api/handlers/member.ts";
|
||||
export * from "./src/api/handlers/message.ts";
|
||||
export * from "./src/api/handlers/webhook.ts";
|
||||
export * from "./src/api/structures/channel.ts";
|
||||
export * from "./src/api/structures/guild.ts";
|
||||
export * from "./src/api/structures/member.ts";
|
||||
export * from "./src/api/structures/message.ts";
|
||||
export * from "./src/api/structures/mod.ts";
|
||||
export * from "./src/api/structures/role.ts";
|
||||
export * from "./src/bot.ts";
|
||||
export * from "./src/rest/mod.ts";
|
||||
export * from "./src/types/mod.ts";
|
||||
export * from "./src/util/cache.ts";
|
||||
export * from "./src/util/collection.ts";
|
||||
export * from "./src/util/permissions.ts";
|
||||
export * from "./src/util/utils.ts";
|
||||
export * from "./src/ws/mod.ts";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { eventHandlers } from "../module/client.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { GuildBanPayload } from "../types/guild.ts";
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { DiscordPayload, GuildBanPayload } from "../../types/mod.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalGuildBanAdd(data: DiscordPayload) {
|
||||
@@ -10,8 +9,8 @@ export async function handleInternalGuildBanAdd(data: DiscordPayload) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
if (!guild) return;
|
||||
|
||||
const member = guild.members.get(payload.user.id);
|
||||
eventHandlers.guildBanAdd?.(guild, member || payload.user);
|
||||
const member = await cacheHandlers.get("members", payload.user.id);
|
||||
eventHandlers.guildBanAdd?.(guild, payload.user, member);
|
||||
}
|
||||
|
||||
export async function handleInternalGuildBanRemove(data: DiscordPayload) {
|
||||
@@ -21,6 +20,6 @@ export async function handleInternalGuildBanRemove(data: DiscordPayload) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
if (!guild) return;
|
||||
|
||||
const member = guild.members.get(payload.user.id);
|
||||
eventHandlers.guildBanRemove?.(guild, member || payload.user);
|
||||
const member = await cacheHandlers.get("members", payload.user.id);
|
||||
eventHandlers.guildBanRemove?.(guild, payload.user, member);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Channel } from "../structures/channel.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Message } from "../structures/message.ts";
|
||||
import { PresenceUpdatePayload } from "../types/discord.ts";
|
||||
import { cache } from "../utils/cache.ts";
|
||||
import { Collection } from "../utils/collection.ts";
|
||||
import { PresenceUpdatePayload } from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { Channel, Guild, Member, Message } from "../structures/structures.ts";
|
||||
|
||||
export type TableName =
|
||||
| "guilds"
|
||||
| "unavailableGuilds"
|
||||
| "channels"
|
||||
| "messages"
|
||||
| "presences"
|
||||
| "unavailableGuilds";
|
||||
| "members"
|
||||
| "presences";
|
||||
|
||||
function set(
|
||||
table: "guilds",
|
||||
@@ -27,6 +26,11 @@ function set(
|
||||
key: string,
|
||||
value: Message,
|
||||
): Promise<Collection<string, Message>>;
|
||||
function set(
|
||||
table: "members",
|
||||
key: string,
|
||||
value: Member,
|
||||
): Promise<Collection<string, Member>>;
|
||||
function set(
|
||||
table: "presences",
|
||||
key: string,
|
||||
@@ -44,6 +48,7 @@ async function set(table: TableName, key: string, value: any) {
|
||||
function get(table: "guilds", key: string): Promise<Guild | undefined>;
|
||||
function get(table: "channels", key: string): Promise<Channel | undefined>;
|
||||
function get(table: "messages", key: string): Promise<Message | undefined>;
|
||||
function get(table: "members", key: string): Promise<Member | undefined>;
|
||||
function get(
|
||||
table: "presences",
|
||||
key: string,
|
||||
@@ -72,6 +77,10 @@ function forEach(
|
||||
table: "messages",
|
||||
callback: (value: Message, key: string, map: Map<string, Message>) => unknown,
|
||||
): void;
|
||||
function forEach(
|
||||
table: "members",
|
||||
callback: (value: Member, key: string, map: Map<string, Member>) => unknown,
|
||||
): void;
|
||||
function forEach(
|
||||
table: TableName,
|
||||
callback: (value: any, key: string, map: Map<string, any>) => unknown,
|
||||
@@ -79,6 +88,33 @@ function forEach(
|
||||
return cache[table].forEach(callback);
|
||||
}
|
||||
|
||||
function filter(
|
||||
table: "guilds",
|
||||
callback: (value: Guild, key: string) => boolean,
|
||||
): Promise<Collection<string, Guild>>;
|
||||
function filter(
|
||||
table: "unavailableGuilds",
|
||||
callback: (value: Guild, key: string) => boolean,
|
||||
): Promise<Collection<string, Guild>>;
|
||||
function filter(
|
||||
table: "channels",
|
||||
callback: (value: Channel, key: string) => boolean,
|
||||
): Promise<Collection<string, Channel>>;
|
||||
function filter(
|
||||
table: "messages",
|
||||
callback: (value: Message, key: string) => boolean,
|
||||
): Promise<Collection<string, Message>>;
|
||||
function filter(
|
||||
table: "members",
|
||||
callback: (value: Member, key: string) => boolean,
|
||||
): Promise<Collection<string, Member>>;
|
||||
async function filter(
|
||||
table: TableName,
|
||||
callback: (value: any, key: string) => boolean,
|
||||
) {
|
||||
return cache[table].filter(callback);
|
||||
}
|
||||
|
||||
export let cacheHandlers = {
|
||||
/** Deletes all items from the cache */
|
||||
clear: async function (table: TableName) {
|
||||
@@ -105,4 +141,6 @@ export let cacheHandlers = {
|
||||
get,
|
||||
/** Run a function on all items in this cache */
|
||||
forEach,
|
||||
/** Allows you to filter our all items in this cache. */
|
||||
filter,
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
import { eventHandlers } from "../module/client.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { ChannelCreatePayload, ChannelTypes } from "../types/channel.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import {
|
||||
ChannelCreatePayload,
|
||||
ChannelTypes,
|
||||
DiscordPayload,
|
||||
} from "../../types/mod.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalChannelCreate(data: DiscordPayload) {
|
||||
@@ -11,11 +14,6 @@ export async function handleInternalChannelCreate(data: DiscordPayload) {
|
||||
const channel = await structures.createChannel(payload);
|
||||
await cacheHandlers.set("channels", channel.id, channel);
|
||||
|
||||
if (channel.guildID) {
|
||||
const guild = await cacheHandlers.get("guilds", channel.guildID);
|
||||
guild?.channels.set(channel.id, channel);
|
||||
}
|
||||
|
||||
eventHandlers.channelCreate?.(channel);
|
||||
}
|
||||
|
||||
@@ -31,20 +29,18 @@ export async function handleInternalChannelDelete(data: DiscordPayload) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
|
||||
if (guild) {
|
||||
guild.voiceStates.forEach((vs, key) => {
|
||||
guild.voiceStates.forEach(async (vs, key) => {
|
||||
if (vs.channelID !== payload.id) return;
|
||||
|
||||
// Since this channel was deleted all voice states for this channel should be deleted
|
||||
guild.voiceStates.delete(key);
|
||||
|
||||
const member = guild.members.get(vs.userID);
|
||||
const member = await cacheHandlers.get("members", vs.userID);
|
||||
if (!member) return;
|
||||
|
||||
eventHandlers.voiceChannelLeave?.(member, vs.channelID);
|
||||
});
|
||||
}
|
||||
|
||||
guild?.channels.delete(payload.id);
|
||||
}
|
||||
|
||||
cacheHandlers.delete("channels", payload.id);
|
||||
@@ -66,10 +62,5 @@ export async function handleInternalChannelUpdate(data: DiscordPayload) {
|
||||
|
||||
if (!cachedChannel) return;
|
||||
|
||||
if (channel.guildID) {
|
||||
const guild = await cacheHandlers.get("guilds", channel.guildID);
|
||||
guild?.channels.set(channel.id, channel);
|
||||
}
|
||||
|
||||
eventHandlers.channelUpdate?.(channel, cachedChannel);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { eventHandlers } from "../module/client.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import {
|
||||
CreateGuildPayload,
|
||||
DiscordPayload,
|
||||
GuildDeletePayload,
|
||||
GuildEmojisUpdatePayload,
|
||||
GuildUpdateChange,
|
||||
UpdateGuildPayload,
|
||||
} from "../types/guild.ts";
|
||||
import { GuildUpdateChange } from "../types/options.ts";
|
||||
import { cache } from "../utils/cache.ts";
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalGuildCreate(
|
||||
25
src/api/controllers/interactions.ts
Normal file
25
src/api/controllers/interactions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { DiscordPayload, InteractionCommandPayload } from "../../types/mod.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
|
||||
export async function handleInternalInteractionsCreate(data: DiscordPayload) {
|
||||
if (data.t !== "INTERACTION_CREATE") return;
|
||||
|
||||
const payload = data.d as InteractionCommandPayload;
|
||||
|
||||
eventHandlers.interactionCreate?.(
|
||||
{
|
||||
...payload,
|
||||
member: await structures.createMember(payload.member, payload.guild_id),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleInternalInteractionsCommandCreate(
|
||||
data: DiscordPayload,
|
||||
) {
|
||||
if (data.t !== "APPLICATION_COMMAND_CREATE") return;
|
||||
|
||||
console.log(data);
|
||||
eventHandlers.interactionCreate?.(data);
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { eventHandlers } from "../module/client.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import {
|
||||
DiscordPayload,
|
||||
GuildBanPayload,
|
||||
GuildMemberAddPayload,
|
||||
GuildMemberChunkPayload,
|
||||
GuildMemberUpdatePayload,
|
||||
} from "../types/guild.ts";
|
||||
import { cache } from "../utils/cache.ts";
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
||||
@@ -20,9 +21,8 @@ export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
||||
guild.memberCount++;
|
||||
const member = await structures.createMember(
|
||||
payload,
|
||||
guild.id,
|
||||
payload.guild_id,
|
||||
);
|
||||
guild.members.set(payload.user.id, member);
|
||||
|
||||
eventHandlers.guildMemberAdd?.(guild, member);
|
||||
}
|
||||
@@ -35,13 +35,11 @@ export async function handleInternalGuildMemberRemove(data: DiscordPayload) {
|
||||
if (!guild) return;
|
||||
|
||||
guild.memberCount--;
|
||||
const member = guild.members.get(payload.user.id);
|
||||
eventHandlers.guildMemberRemove?.(
|
||||
guild,
|
||||
member || payload.user,
|
||||
);
|
||||
const member = await cacheHandlers.get("members", payload.user.id);
|
||||
eventHandlers.guildMemberRemove?.(guild, payload.user, member);
|
||||
|
||||
guild.members.delete(payload.user.id);
|
||||
member?.guilds.delete(guild.id);
|
||||
if (member && !member.guilds.size) cacheHandlers.delete("members", member.id);
|
||||
}
|
||||
|
||||
export async function handleInternalGuildMemberUpdate(data: DiscordPayload) {
|
||||
@@ -51,31 +49,32 @@ export async function handleInternalGuildMemberUpdate(data: DiscordPayload) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
if (!guild) return;
|
||||
|
||||
const cachedMember = guild.members.get(payload.user.id);
|
||||
const cachedMember = await cacheHandlers.get("members", payload.user.id);
|
||||
const guildMember = cachedMember?.guilds.get(payload.guild_id);
|
||||
|
||||
const newMemberData = {
|
||||
...payload,
|
||||
premium_since: payload.premium_since || undefined,
|
||||
joined_at: new Date(cachedMember?.joinedAt || Date.now())
|
||||
joined_at: new Date(guildMember?.joinedAt || Date.now())
|
||||
.toISOString(),
|
||||
deaf: cachedMember?.deaf || false,
|
||||
mute: cachedMember?.mute || false,
|
||||
deaf: guildMember?.deaf || false,
|
||||
mute: guildMember?.mute || false,
|
||||
roles: payload.roles,
|
||||
};
|
||||
const member = await structures.createMember(
|
||||
newMemberData,
|
||||
guild.id,
|
||||
payload.guild_id,
|
||||
);
|
||||
guild.members.set(payload.user.id, member);
|
||||
|
||||
if (cachedMember?.nick !== payload.nick) {
|
||||
if (guildMember?.nick !== payload.nick) {
|
||||
eventHandlers.nicknameUpdate?.(
|
||||
guild,
|
||||
member,
|
||||
payload.nick,
|
||||
cachedMember?.nick,
|
||||
guildMember?.nick,
|
||||
);
|
||||
}
|
||||
const roleIDs = cachedMember?.roles || [];
|
||||
const roleIDs = guildMember?.roles || [];
|
||||
|
||||
roleIDs.forEach((id) => {
|
||||
if (!payload.roles.includes(id)) {
|
||||
@@ -96,18 +95,12 @@ export async function handleInternalGuildMembersChunk(data: DiscordPayload) {
|
||||
if (data.t !== "GUILD_MEMBERS_CHUNK") return;
|
||||
|
||||
const payload = data.d as GuildMemberChunkPayload;
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
if (!guild) return;
|
||||
|
||||
payload.members.forEach(async (member) => {
|
||||
guild.members.set(
|
||||
member.user.id,
|
||||
await structures.createMember(
|
||||
member,
|
||||
guild.id,
|
||||
),
|
||||
);
|
||||
});
|
||||
const members = await Promise.all(
|
||||
payload.members.map((member) =>
|
||||
structures.createMember(member, payload.guild_id)
|
||||
),
|
||||
);
|
||||
|
||||
// Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming
|
||||
if (
|
||||
@@ -118,7 +111,17 @@ export async function handleInternalGuildMembersChunk(data: DiscordPayload) {
|
||||
|
||||
if (payload.chunk_index + 1 === payload.chunk_count) {
|
||||
cache.fetchAllMembersProcessingRequests.delete(payload.nonce);
|
||||
resolve(guild.members);
|
||||
// Only 1 chunk most likely is all members or users only request a small amount of users
|
||||
if (payload.chunk_count === 1) {
|
||||
return resolve(new Collection(members.map((m) => [m.id, m])));
|
||||
}
|
||||
|
||||
return resolve(
|
||||
await cacheHandlers.filter(
|
||||
"members",
|
||||
(m) => m.guilds.has(payload.guild_id),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { eventHandlers } from "../module/client.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import {
|
||||
DiscordPayload,
|
||||
MessageCreateOptions,
|
||||
MessageDeleteBulkPayload,
|
||||
MessageDeletePayload,
|
||||
} from "../types/message.ts";
|
||||
} from "../../types/mod.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalMessageCreate(data: DiscordPayload) {
|
||||
@@ -19,26 +19,20 @@ export async function handleInternalMessageCreate(data: DiscordPayload) {
|
||||
? await cacheHandlers.get("guilds", payload.guild_id)
|
||||
: undefined;
|
||||
|
||||
if (payload.member) {
|
||||
if (payload.member && guild) {
|
||||
// If in a guild cache the author as a member
|
||||
guild?.members.set(
|
||||
payload.author.id,
|
||||
await structures.createMember(
|
||||
{ ...payload.member, user: payload.author },
|
||||
guild.id,
|
||||
),
|
||||
await structures.createMember(
|
||||
{ ...payload.member, user: payload.author },
|
||||
guild.id,
|
||||
);
|
||||
}
|
||||
|
||||
payload.mentions.forEach(async (mention) => {
|
||||
payload.mentions.forEach((mention) => {
|
||||
// Cache the member if its a valid member
|
||||
if (mention.member) {
|
||||
guild?.members.set(
|
||||
mention.id,
|
||||
await structures.createMember(
|
||||
{ ...mention.member, user: mention },
|
||||
guild.id,
|
||||
),
|
||||
if (mention.member && guild) {
|
||||
structures.createMember(
|
||||
{ ...mention.member, user: mention },
|
||||
guild.id,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -58,8 +52,8 @@ export async function handleInternalMessageDelete(data: DiscordPayload) {
|
||||
if (!channel) return;
|
||||
|
||||
eventHandlers.messageDelete?.(
|
||||
await cacheHandlers.get("messages", payload.id) ||
|
||||
{ id: payload.id, channel },
|
||||
{ id: payload.id, channel },
|
||||
await cacheHandlers.get("messages", payload.id),
|
||||
);
|
||||
|
||||
cacheHandlers.delete("messages", payload.id);
|
||||
@@ -74,7 +68,8 @@ export async function handleInternalMessageDeleteBulk(data: DiscordPayload) {
|
||||
|
||||
payload.ids.forEach(async (id) => {
|
||||
eventHandlers.messageDelete?.(
|
||||
await cacheHandlers.get("messages", id) || { id, channel },
|
||||
{ id, channel },
|
||||
await cacheHandlers.get("messages", id),
|
||||
);
|
||||
cacheHandlers.delete("messages", id);
|
||||
});
|
||||
@@ -1,19 +1,23 @@
|
||||
import { delay } from "../../deps.ts";
|
||||
import { eventHandlers, setBotID } from "../module/client.ts";
|
||||
import { allowNextShard } from "../module/shardingManager.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { eventHandlers, setBotID } from "../../bot.ts";
|
||||
import {
|
||||
DiscordPayload,
|
||||
PresenceUpdatePayload,
|
||||
ReadyPayload,
|
||||
TypingStartPayload,
|
||||
UserPayload,
|
||||
VoiceStateUpdatePayload,
|
||||
WebhookUpdatePayload,
|
||||
} from "../types/discord.ts";
|
||||
import { UserPayload } from "../types/guild.ts";
|
||||
import { cache } from "../utils/cache.ts";
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { delay } from "../../util/utils.ts";
|
||||
import { allowNextShard } from "../../ws/shard_manager.ts";
|
||||
import {
|
||||
initialMemberLoadQueue,
|
||||
structures,
|
||||
} from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
/** This function is the internal handler for the ready event. Users can override this with controllers if desired. */
|
||||
export async function handleInternalReady(
|
||||
data: DiscordPayload,
|
||||
shardID: number,
|
||||
@@ -30,6 +34,13 @@ export async function handleInternalReady(
|
||||
await delay(5000);
|
||||
cache.isReady = true;
|
||||
eventHandlers.ready?.();
|
||||
|
||||
// All the members that came in on guild creates should now be processed 1 by 1
|
||||
for (const [guildID, members] of initialMemberLoadQueue.entries()) {
|
||||
await Promise.all(
|
||||
members.map((member) => structures.createMember(member, guildID)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait 5 seconds to spawn next shard
|
||||
@@ -37,6 +48,7 @@ export async function handleInternalReady(
|
||||
allowNextShard();
|
||||
}
|
||||
|
||||
/** This function is the internal handler for the presence update event. Users can override this with controllers if desired. */
|
||||
export async function handleInternalPresenceUpdate(data: DiscordPayload) {
|
||||
if (data.t !== "PRESENCE_UPDATE") return;
|
||||
|
||||
@@ -47,30 +59,29 @@ export async function handleInternalPresenceUpdate(data: DiscordPayload) {
|
||||
return eventHandlers.presenceUpdate?.(payload, oldPresence);
|
||||
}
|
||||
|
||||
/** This function is the internal handler for the typings event. Users can override this with controllers if desired. */
|
||||
export function handleInternalTypingStart(data: DiscordPayload) {
|
||||
if (data.t !== "TYPING_START") return;
|
||||
eventHandlers.typingStart?.(data.d as TypingStartPayload);
|
||||
}
|
||||
|
||||
export function handleInternalUserUpdate(data: DiscordPayload) {
|
||||
/** This function is the internal handler for the user update event. Users can override this with controllers if desired. */
|
||||
export async function handleInternalUserUpdate(data: DiscordPayload) {
|
||||
if (data.t !== "USER_UPDATE") return;
|
||||
|
||||
const userData = data.d as UserPayload;
|
||||
|
||||
cacheHandlers.forEach("guilds", (guild) => {
|
||||
const member = guild.members.get(userData.id);
|
||||
if (!member) return;
|
||||
// member.author = userData;
|
||||
Object.entries(userData).forEach(([key, value]) => {
|
||||
// @ts-ignore
|
||||
if (member[key] === value) return;
|
||||
// @ts-ignore
|
||||
member[key] = value;
|
||||
});
|
||||
const member = await cacheHandlers.get("members", userData.id);
|
||||
if (!member) return;
|
||||
|
||||
Object.entries(userData).forEach(([key, value]) => {
|
||||
// @ts-ignore
|
||||
if (member[key] !== value) return member[key] = value;
|
||||
});
|
||||
return eventHandlers.botUpdate?.(userData);
|
||||
}
|
||||
|
||||
/** This function is the internal handler for the voice state update event. Users can override this with controllers if desired. */
|
||||
export async function handleInternalVoiceStateUpdate(data: DiscordPayload) {
|
||||
if (data.t !== "VOICE_STATE_UPDATE") return;
|
||||
|
||||
@@ -80,10 +91,9 @@ export async function handleInternalVoiceStateUpdate(data: DiscordPayload) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
if (!guild) return;
|
||||
|
||||
const member = guild.members.get(payload.user_id) ||
|
||||
(payload.member
|
||||
? await structures.createMember(payload.member, guild.id)
|
||||
: undefined);
|
||||
const member = payload.member
|
||||
? await structures.createMember(payload.member, guild.id)
|
||||
: await cacheHandlers.get("members", payload.user_id);
|
||||
if (!member) return;
|
||||
|
||||
// No cached state before so lets make one for em
|
||||
@@ -92,12 +102,12 @@ export async function handleInternalVoiceStateUpdate(data: DiscordPayload) {
|
||||
guild.voiceStates.set(payload.user_id, {
|
||||
...payload,
|
||||
guildID: payload.guild_id,
|
||||
channelID: payload.channel_id,
|
||||
channelID: payload.channel_id || "",
|
||||
userID: payload.user_id,
|
||||
sessionID: payload.session_id,
|
||||
selfDeaf: payload.self_deaf,
|
||||
selfMute: payload.self_mute,
|
||||
selfStream: payload.self_stream,
|
||||
selfStream: payload.self_stream || false,
|
||||
});
|
||||
|
||||
if (cachedState?.channelID !== payload.channel_id) {
|
||||
@@ -122,6 +132,7 @@ export async function handleInternalVoiceStateUpdate(data: DiscordPayload) {
|
||||
eventHandlers.voiceStateUpdate?.(member, payload);
|
||||
}
|
||||
|
||||
/** This function is the internal handler for the webhooks update event. Users can override this with controllers if desired. */
|
||||
export function handleInternalWebhooksUpdate(data: DiscordPayload) {
|
||||
if (data.t !== "WEBHOOKS_UPDATE") return;
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
handleInternalGuildEmojisUpdate,
|
||||
handleInternalGuildUpdate,
|
||||
} from "./guilds.ts";
|
||||
import {
|
||||
handleInternalInteractionsCommandCreate,
|
||||
handleInternalInteractionsCreate,
|
||||
} from "./interactions.ts";
|
||||
import {
|
||||
handleInternalGuildMemberAdd,
|
||||
handleInternalGuildMemberRemove,
|
||||
@@ -63,6 +67,8 @@ export let controllers = {
|
||||
GUILD_ROLE_CREATE: handleInternalGuildRoleCreate,
|
||||
GUILD_ROLE_DELETE: handleInternalGuildRoleDelete,
|
||||
GUILD_ROLE_UPDATE: handleInternalGuildRoleUpdate,
|
||||
INTERACTION_CREATE: handleInternalInteractionsCreate,
|
||||
APPLICATION_COMMAND_CREATE: handleInternalInteractionsCommandCreate,
|
||||
MESSAGE_CREATE: handleInternalMessageCreate,
|
||||
MESSAGE_DELETE: handleInternalMessageDelete,
|
||||
MESSAGE_DELETE_BULK: handleInternalMessageDeleteBulk,
|
||||
@@ -1,11 +1,11 @@
|
||||
import { botID, eventHandlers } from "../module/client.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { botID, eventHandlers } from "../../bot.ts";
|
||||
import {
|
||||
BaseMessageReactionPayload,
|
||||
DiscordPayload,
|
||||
MessageReactionPayload,
|
||||
MessageReactionRemoveEmojiPayload,
|
||||
} from "../types/message.ts";
|
||||
} from "../../types/mod.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
||||
@@ -39,26 +39,23 @@ export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
||||
|
||||
if (payload.member && payload.guild_id) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
guild?.members.set(
|
||||
payload.member.user.id,
|
||||
await structures.createMember(
|
||||
payload.member,
|
||||
guild.id,
|
||||
),
|
||||
);
|
||||
if (guild) {
|
||||
await structures.createMember(payload.member, guild.id);
|
||||
}
|
||||
}
|
||||
|
||||
const uncachedOptions = {
|
||||
...payload,
|
||||
id: payload.message_id,
|
||||
channelID: payload.channel_id,
|
||||
guildID: payload.guild_id,
|
||||
guildID: payload.guild_id || "",
|
||||
};
|
||||
|
||||
eventHandlers.reactionAdd?.(
|
||||
message || uncachedOptions,
|
||||
uncachedOptions,
|
||||
payload.emoji,
|
||||
payload.user_id,
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,13 +92,12 @@ export async function handleInternalMessageReactionRemove(
|
||||
|
||||
if (payload.member && payload.guild_id) {
|
||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||
guild?.members.set(
|
||||
payload.member.user.id,
|
||||
if (guild) {
|
||||
await structures.createMember(
|
||||
payload.member,
|
||||
guild.id,
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const uncachedOptions = {
|
||||
@@ -112,9 +108,10 @@ export async function handleInternalMessageReactionRemove(
|
||||
};
|
||||
|
||||
eventHandlers.reactionRemove?.(
|
||||
message || uncachedOptions,
|
||||
uncachedOptions,
|
||||
payload.emoji,
|
||||
payload.user_id,
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { eventHandlers } from "../module/client.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { DiscordPayload } from "../types/discord.ts";
|
||||
import { GuildRoleDeletePayload, GuildRolePayload } from "../types/guild.ts";
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import {
|
||||
DiscordPayload,
|
||||
GuildRoleDeletePayload,
|
||||
GuildRolePayload,
|
||||
} from "../../types/mod.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
import { cacheHandlers } from "./cache.ts";
|
||||
|
||||
export async function handleInternalGuildRoleCreate(data: DiscordPayload) {
|
||||
@@ -29,8 +32,16 @@ export async function handleInternalGuildRoleDelete(data: DiscordPayload) {
|
||||
eventHandlers.roleDelete?.(guild, cachedRole);
|
||||
|
||||
// For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking.
|
||||
guild.members.forEach((member) => {
|
||||
member.roles = member.roles.filter((id) => id !== payload.role_id);
|
||||
cacheHandlers.forEach("members", (member) => {
|
||||
// Not in the relevant guild so just skip.
|
||||
if (!member.guilds.has(guild.id)) return;
|
||||
|
||||
member.guilds.forEach((g) => {
|
||||
// Member does not have this role
|
||||
if (!g.roles.includes(payload.role_id)) return;
|
||||
// Remove this role from the members cache
|
||||
g.roles = g.roles.filter((id) => id !== payload.role_id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { RequestManager } from "../module/requestManager.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { RequestManager } from "../../rest/mod.ts";
|
||||
import {
|
||||
ChannelEditOptions,
|
||||
ChannelTypes,
|
||||
CreateInviteOptions,
|
||||
Errors,
|
||||
FollowedChannelPayload,
|
||||
GetMessages,
|
||||
GetMessagesAfter,
|
||||
GetMessagesAround,
|
||||
GetMessagesBefore,
|
||||
MessageContent,
|
||||
} from "../types/channel.ts";
|
||||
import { Errors } from "../types/errors.ts";
|
||||
import { RawOverwrite } from "../types/guild.ts";
|
||||
import { MessageCreateOptions } from "../types/message.ts";
|
||||
import { Permissions } from "../types/permission.ts";
|
||||
import { endpoints } from "../utils/constants.ts";
|
||||
import { botHasChannelPermissions } from "../utils/permissions.ts";
|
||||
MessageCreateOptions,
|
||||
Permission,
|
||||
Permissions,
|
||||
RawOverwrite,
|
||||
WebhookPayload,
|
||||
} from "../../types/mod.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import {
|
||||
botHasChannelPermissions,
|
||||
calculateBits,
|
||||
} from "../../util/permissions.ts";
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
|
||||
/** Checks if a channel overwrite for a user id or a role id has permission in this channel */
|
||||
export function channelOverwriteHasPermission(
|
||||
guildID: string,
|
||||
id: string,
|
||||
overwrites: RawOverwrite[],
|
||||
permissions: Permissions[],
|
||||
permissions: Permission[],
|
||||
) {
|
||||
const overwrite = overwrites.find((perm) => perm.id === id) ||
|
||||
overwrites.find((perm) => perm.id === guildID);
|
||||
|
||||
return permissions.every((perm) => {
|
||||
if (overwrite) {
|
||||
if (BigInt(overwrite.deny) & BigInt(perm)) return false;
|
||||
if (BigInt(overwrite.allow) & BigInt(perm)) return true;
|
||||
const allowBits = overwrite.allow;
|
||||
const denyBits = overwrite.deny;
|
||||
if (BigInt(denyBits) & BigInt(Permissions[perm])) return false;
|
||||
if (BigInt(allowBits) & BigInt(Permissions[perm])) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -45,7 +52,7 @@ export async function getMessage(
|
||||
) {
|
||||
const hasViewChannelPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.VIEW_CHANNEL],
|
||||
["VIEW_CHANNEL"],
|
||||
);
|
||||
if (
|
||||
!hasViewChannelPerm
|
||||
@@ -55,7 +62,7 @@ export async function getMessage(
|
||||
|
||||
const hasReadMessageHistoryPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.READ_MESSAGE_HISTORY],
|
||||
["READ_MESSAGE_HISTORY"],
|
||||
);
|
||||
if (
|
||||
!hasReadMessageHistoryPerm
|
||||
@@ -80,7 +87,7 @@ export async function getMessages(
|
||||
) {
|
||||
const hasViewChannelPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.VIEW_CHANNEL],
|
||||
["VIEW_CHANNEL"],
|
||||
);
|
||||
if (
|
||||
!hasViewChannelPerm
|
||||
@@ -90,7 +97,7 @@ export async function getMessages(
|
||||
|
||||
const hasReadMessageHistoryPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.READ_MESSAGE_HISTORY],
|
||||
["READ_MESSAGE_HISTORY"],
|
||||
);
|
||||
if (
|
||||
!hasReadMessageHistoryPerm
|
||||
@@ -123,7 +130,7 @@ export async function sendMessage(
|
||||
if (typeof content === "string") content = { content };
|
||||
const hasSendMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.SEND_MESSAGES],
|
||||
["SEND_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasSendMessagesPerm
|
||||
@@ -133,7 +140,7 @@ export async function sendMessage(
|
||||
|
||||
const hasSendTtsMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.SEND_TTS_MESSAGES],
|
||||
["SEND_TTS_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
content.tts &&
|
||||
@@ -144,7 +151,7 @@ export async function sendMessage(
|
||||
|
||||
const hasEmbedLinksPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.EMBED_LINKS],
|
||||
["EMBED_LINKS"],
|
||||
);
|
||||
if (
|
||||
content.embed &&
|
||||
@@ -187,10 +194,10 @@ export async function sendMessage(
|
||||
if (
|
||||
!(await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.READ_MESSAGE_HISTORY],
|
||||
["READ_MESSAGE_HISTORY"],
|
||||
))
|
||||
) {
|
||||
throw new Error(Errors.MISSING_SEND_MESSAGES);
|
||||
throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +218,7 @@ export async function sendMessage(
|
||||
allowed_mentions: content.mentions
|
||||
? {
|
||||
...content.mentions,
|
||||
replied_user: content.mentions.repliedUser !== false,
|
||||
replied_user: content.mentions.repliedUser,
|
||||
}
|
||||
: undefined,
|
||||
message_reference: {
|
||||
@@ -231,7 +238,7 @@ export async function deleteMessages(
|
||||
) {
|
||||
const hasManageMessages = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasManageMessages
|
||||
@@ -258,7 +265,7 @@ export async function deleteMessages(
|
||||
export async function getChannelInvites(channelID: string) {
|
||||
const hasManagaChannels = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_CHANNELS],
|
||||
["MANAGE_CHANNELS"],
|
||||
);
|
||||
if (
|
||||
!hasManagaChannels
|
||||
@@ -275,7 +282,7 @@ export async function createInvite(
|
||||
) {
|
||||
const hasCreateInstantInvitePerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.CREATE_INSTANT_INVITE],
|
||||
["CREATE_INSTANT_INVITE"],
|
||||
);
|
||||
if (
|
||||
!hasCreateInstantInvitePerm
|
||||
@@ -289,14 +296,16 @@ export async function createInvite(
|
||||
export async function getChannelWebhooks(channelID: string) {
|
||||
const hasManageWebhooksPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_WEBHOOKS],
|
||||
["MANAGE_WEBHOOKS"],
|
||||
);
|
||||
if (
|
||||
!hasManageWebhooksPerm
|
||||
) {
|
||||
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
|
||||
}
|
||||
return RequestManager.get(endpoints.CHANNEL_WEBHOOKS(channelID));
|
||||
return RequestManager.get(endpoints.CHANNEL_WEBHOOKS(channelID)) as Promise<
|
||||
WebhookPayload[]
|
||||
>;
|
||||
}
|
||||
|
||||
interface EditChannelRequest {
|
||||
@@ -352,7 +361,7 @@ export async function editChannel(
|
||||
) {
|
||||
const hasManageChannelsPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_CHANNELS],
|
||||
["MANAGE_CHANNELS"],
|
||||
);
|
||||
if (
|
||||
!hasManageChannelsPerm
|
||||
@@ -394,14 +403,8 @@ export async function editChannel(
|
||||
(overwrite) => {
|
||||
return {
|
||||
...overwrite,
|
||||
allow: overwrite.allow.reduce(
|
||||
(bits, perm) => bits |= BigInt(Permissions[perm]),
|
||||
BigInt(0),
|
||||
).toString(),
|
||||
deny: overwrite.deny.reduce(
|
||||
(bits, perm) => bits |= BigInt(Permissions[perm]),
|
||||
BigInt(0),
|
||||
).toString(),
|
||||
allow: calculateBits(overwrite.allow),
|
||||
deny: calculateBits(overwrite.deny),
|
||||
};
|
||||
},
|
||||
),
|
||||
@@ -423,7 +426,7 @@ export async function followChannel(
|
||||
) {
|
||||
const hasManageWebhooksPerm = await botHasChannelPermissions(
|
||||
targetChannelID,
|
||||
[Permissions.MANAGE_WEBHOOKS],
|
||||
["MANAGE_WEBHOOKS"],
|
||||
);
|
||||
if (
|
||||
!hasManageWebhooksPerm
|
||||
@@ -453,8 +456,8 @@ export async function isChannelSynced(channelID: string) {
|
||||
const parentChannel = await cacheHandlers.get("channels", channel.parentID);
|
||||
if (!parentChannel) return false;
|
||||
|
||||
return channel.permission_overwrites?.every((overwrite) => {
|
||||
const permission = parentChannel.permission_overwrites?.find((ow) =>
|
||||
return channel.permissionOverwrites?.every((overwrite) => {
|
||||
const permission = parentChannel.permissionOverwrites?.find((ow) =>
|
||||
ow.id === overwrite.id
|
||||
);
|
||||
if (!permission) return false;
|
||||
@@ -1,18 +1,12 @@
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { identifyPayload } from "../module/client.ts";
|
||||
import { RequestManager } from "../module/requestManager.ts";
|
||||
import { requestAllMembers } from "../module/shardingManager.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Member } from "../structures/member.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { Template } from "../structures/template.ts";
|
||||
import { ImageFormats, ImageSize } from "../types/cdn.ts";
|
||||
import { ChannelCreatePayload, ChannelTypes } from "../types/channel.ts";
|
||||
import { Errors } from "../types/errors.ts";
|
||||
import { identifyPayload } from "../../bot.ts";
|
||||
import { RequestManager } from "../../rest/mod.ts";
|
||||
import {
|
||||
AuditLogs,
|
||||
BannedUser,
|
||||
BanOptions,
|
||||
ChannelCreateOptions,
|
||||
ChannelCreatePayload,
|
||||
ChannelTypes,
|
||||
CreateEmojisOptions,
|
||||
CreateGuildFromTemplate,
|
||||
CreateGuildPayload,
|
||||
@@ -22,25 +16,34 @@ import {
|
||||
EditEmojisOptions,
|
||||
EditGuildTemplate,
|
||||
EditIntegrationOptions,
|
||||
Errors,
|
||||
FetchMembersOptions,
|
||||
GetAuditLogsOptions,
|
||||
GuildEditOptions,
|
||||
GuildTemplate,
|
||||
ImageFormats,
|
||||
ImageSize,
|
||||
Intents,
|
||||
MemberCreatePayload,
|
||||
PositionSwap,
|
||||
PruneOptions,
|
||||
PrunePayload,
|
||||
RoleData,
|
||||
UpdateGuildPayload,
|
||||
UserPayload,
|
||||
} from "../types/guild.ts";
|
||||
import { MemberCreatePayload } from "../types/member.ts";
|
||||
import { Intents } from "../types/options.ts";
|
||||
import { Permissions } from "../types/permission.ts";
|
||||
import { RoleData } from "../types/role.ts";
|
||||
import { formatImageURL } from "../utils/cdn.ts";
|
||||
import { Collection } from "../utils/collection.ts";
|
||||
import { endpoints } from "../utils/constants.ts";
|
||||
import { botHasPermission, calculateBits } from "../utils/permissions.ts";
|
||||
import { urlToBase64 } from "../utils/utils.ts";
|
||||
} from "../../types/mod.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import { botHasPermission, calculateBits } from "../../util/permissions.ts";
|
||||
import { formatImageURL, urlToBase64 } from "../../util/utils.ts";
|
||||
import { requestAllMembers } from "../../ws/shard_manager.ts";
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import {
|
||||
Guild,
|
||||
Member,
|
||||
structures,
|
||||
Template,
|
||||
} from "../structures/structures.ts";
|
||||
|
||||
/** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */
|
||||
export async function createServer(options: CreateServerOptions) {
|
||||
@@ -58,8 +61,11 @@ export function deleteServer(guildID: string) {
|
||||
}
|
||||
|
||||
/** Gets an array of all the channels ids that are the children of this category. */
|
||||
export function categoryChildrenIDs(guild: Guild, id: string) {
|
||||
return guild.channels.filter((channel) => channel.parentID === id);
|
||||
export function categoryChildrenIDs(guildID: string, id: string) {
|
||||
return cacheHandlers.filter(
|
||||
"channels",
|
||||
(channel) => channel.parentID === id && channel.guildID === guildID,
|
||||
);
|
||||
}
|
||||
|
||||
/** The full URL of the icon from Discords CDN. Undefined when no icon is set. */
|
||||
@@ -110,7 +116,7 @@ export async function createGuildChannel(
|
||||
) {
|
||||
const hasPerm = await botHasPermission(
|
||||
guild.id,
|
||||
[Permissions.MANAGE_CHANNELS],
|
||||
["MANAGE_CHANNELS"],
|
||||
);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
|
||||
@@ -120,23 +126,16 @@ export async function createGuildChannel(
|
||||
(await RequestManager.post(endpoints.GUILD_CHANNELS(guild.id), {
|
||||
...options,
|
||||
name,
|
||||
permission_overwrites: options?.permission_overwrites?.map((perm) => ({
|
||||
permission_overwrites: options?.permissionOverwrites?.map((perm) => ({
|
||||
...perm,
|
||||
|
||||
allow: perm.allow.reduce(
|
||||
(bits, p) => bits |= BigInt(Permissions[p]),
|
||||
BigInt(0),
|
||||
).toString(),
|
||||
deny: perm.deny.reduce(
|
||||
(bits, p) => bits |= BigInt(Permissions[p]),
|
||||
BigInt(0),
|
||||
).toString(),
|
||||
allow: calculateBits(perm.allow),
|
||||
deny: calculateBits(perm.deny),
|
||||
})),
|
||||
type: options?.type || ChannelTypes.GUILD_TEXT,
|
||||
})) as ChannelCreatePayload;
|
||||
|
||||
const channel = await structures.createChannel(result);
|
||||
guild.channels.set(result.id, channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
@@ -148,7 +147,7 @@ export async function deleteChannel(
|
||||
) {
|
||||
const hasPerm = await botHasPermission(
|
||||
guildID,
|
||||
[Permissions.MANAGE_CHANNELS],
|
||||
["MANAGE_CHANNELS"],
|
||||
);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
|
||||
@@ -218,7 +217,6 @@ export async function getMember(
|
||||
) as MemberCreatePayload;
|
||||
|
||||
const member = await structures.createMember(data, guildID);
|
||||
guild?.members.set(id, member);
|
||||
return member;
|
||||
}
|
||||
|
||||
@@ -246,7 +244,7 @@ export async function createEmoji(
|
||||
image: string,
|
||||
options: CreateEmojisOptions,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_EMOJIS);
|
||||
}
|
||||
@@ -268,7 +266,7 @@ export async function editEmoji(
|
||||
id: string,
|
||||
options: EditEmojisOptions,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_EMOJIS);
|
||||
}
|
||||
@@ -285,7 +283,7 @@ export async function deleteEmoji(
|
||||
id: string,
|
||||
reason?: string,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_EMOJIS);
|
||||
}
|
||||
@@ -307,20 +305,18 @@ export async function createGuildRole(
|
||||
options: CreateRoleOptions,
|
||||
reason?: string,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
|
||||
const bits = calculateBits(options.permissions || []);
|
||||
|
||||
const result = await RequestManager.post(
|
||||
endpoints.GUILD_ROLES(guildID),
|
||||
{
|
||||
...options,
|
||||
permissions: options.permissions
|
||||
?.reduce((subtotal, perm) => {
|
||||
subtotal |= Permissions[perm];
|
||||
return subtotal;
|
||||
}, 0),
|
||||
permissions: calculateBits(options?.permissions || []),
|
||||
reason,
|
||||
},
|
||||
);
|
||||
@@ -338,7 +334,7 @@ export async function editRole(
|
||||
id: string,
|
||||
options: CreateRoleOptions,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
@@ -353,7 +349,7 @@ export async function editRole(
|
||||
|
||||
/** Delete a guild role. Requires the MANAGE_ROLES permission. */
|
||||
export async function deleteRole(guildID: string, id: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
@@ -366,7 +362,7 @@ export async function deleteRole(guildID: string, id: string) {
|
||||
* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.**
|
||||
*/
|
||||
export async function getRoles(guildID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
@@ -376,7 +372,7 @@ export async function getRoles(guildID: string) {
|
||||
|
||||
/** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */
|
||||
export async function swapRoles(guildID: string, rolePositons: PositionSwap) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
@@ -390,7 +386,7 @@ export async function getPruneCount(guildID: string, options: PruneOptions) {
|
||||
throw new Error(Errors.PRUNE_MIN_DAYS);
|
||||
}
|
||||
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.KICK_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_KICK_MEMBERS);
|
||||
}
|
||||
@@ -409,7 +405,7 @@ export async function pruneMembers(guildID: string, options: PruneOptions) {
|
||||
throw new Error(Errors.PRUNE_MIN_DAYS);
|
||||
}
|
||||
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.KICK_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_KICK_MEMBERS);
|
||||
}
|
||||
@@ -420,11 +416,27 @@ export async function pruneMembers(guildID: string, options: PruneOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ BEGINNER DEVS!! YOU SHOULD ALMOST NEVER NEED THIS AND YOU CAN GET FROM cache.members.get()
|
||||
*
|
||||
* ADVANCED:
|
||||
* Highly recommended to use this function to fetch members instead of getMember from REST.
|
||||
* REST: 50/s global(across all shards) rate limit with ALL requests this included
|
||||
* GW(this function): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is now 960/m.
|
||||
*/
|
||||
export function fetchMembers(guild: Guild, options?: FetchMembersOptions) {
|
||||
if (!(identifyPayload.intents & Intents.GUILD_MEMBERS)) {
|
||||
// You can request 1 member without the intent
|
||||
if (
|
||||
(!options?.limit || options.limit > 1) &&
|
||||
!(identifyPayload.intents & Intents.GUILD_MEMBERS)
|
||||
) {
|
||||
throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS);
|
||||
}
|
||||
|
||||
if (options?.userIDs?.length) {
|
||||
options.limit = options.userIDs.length;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
requestAllMembers(guild, resolve, options);
|
||||
}) as Promise<Collection<string, Member>>;
|
||||
@@ -435,13 +447,16 @@ export async function getAuditLogs(
|
||||
guildID: string,
|
||||
options: GetAuditLogsOptions,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.VIEW_AUDIT_LOG]);
|
||||
const hasPerm = await botHasPermission(guildID, ["VIEW_AUDIT_LOG"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_VIEW_AUDIT_LOG);
|
||||
}
|
||||
|
||||
return RequestManager.get(endpoints.GUILD_AUDIT_LOGS(guildID), {
|
||||
...options,
|
||||
action_type: options.action_type
|
||||
? AuditLogs[options.action_type]
|
||||
: undefined,
|
||||
limit: options.limit && options.limit >= 1 && options.limit <= 100
|
||||
? options.limit
|
||||
: 50,
|
||||
@@ -450,7 +465,7 @@ export async function getAuditLogs(
|
||||
|
||||
/** Returns the guild embed object. Requires the MANAGE_GUILD permission. */
|
||||
export async function getEmbed(guildID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -464,7 +479,7 @@ export async function editEmbed(
|
||||
enabled: boolean,
|
||||
channelID?: string | null,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -482,7 +497,7 @@ export function getVanityURL(guildID: string) {
|
||||
|
||||
/** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */
|
||||
export async function getIntegrations(guildID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -496,7 +511,7 @@ export async function editIntegration(
|
||||
id: string,
|
||||
options: EditIntegrationOptions,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -509,7 +524,7 @@ export async function editIntegration(
|
||||
|
||||
/** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */
|
||||
export async function deleteIntegration(guildID: string, id: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -519,7 +534,7 @@ export async function deleteIntegration(guildID: string, id: string) {
|
||||
|
||||
/** Sync an integration. Requires the MANAGE_GUILD permission. */
|
||||
export async function syncIntegration(guildID: string, id: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -529,7 +544,7 @@ export async function syncIntegration(guildID: string, id: string) {
|
||||
|
||||
/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */
|
||||
export async function getBans(guildID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_BAN_MEMBERS);
|
||||
}
|
||||
@@ -545,7 +560,7 @@ export async function getBans(guildID: string) {
|
||||
|
||||
/** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */
|
||||
export async function getBan(guildID: string, memberID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_BAN_MEMBERS);
|
||||
}
|
||||
@@ -557,7 +572,7 @@ export async function getBan(guildID: string, memberID: string) {
|
||||
|
||||
/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */
|
||||
export async function ban(guildID: string, id: string, options: BanOptions) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_BAN_MEMBERS);
|
||||
}
|
||||
@@ -570,7 +585,7 @@ export async function ban(guildID: string, id: string, options: BanOptions) {
|
||||
|
||||
/** Remove the ban for a user. Requires BAN_MEMBERS permission */
|
||||
export async function unban(guildID: string, id: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_BAN_MEMBERS);
|
||||
}
|
||||
@@ -579,7 +594,7 @@ export async function unban(guildID: string, id: string) {
|
||||
|
||||
/** Modify a guilds settings. Requires the MANAGE_GUILD permission. */
|
||||
export async function editGuild(guildID: string, options: GuildEditOptions) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -601,7 +616,7 @@ export async function editGuild(guildID: string, options: GuildEditOptions) {
|
||||
|
||||
/** Get all the invites for this guild. Requires MANAGE_GUILD permission */
|
||||
export async function getInvites(guildID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
}
|
||||
@@ -623,7 +638,7 @@ export function getVoiceRegions(guildID: string) {
|
||||
export async function getWebhooks(guildID: string) {
|
||||
const hasPerm = await botHasPermission(
|
||||
guildID,
|
||||
[Permissions.MANAGE_WEBHOOKS],
|
||||
["MANAGE_WEBHOOKS"],
|
||||
);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
|
||||
@@ -691,7 +706,7 @@ export async function createGuildFromTemplate(
|
||||
* Requires the `MANAGE_GUILD` permission.
|
||||
*/
|
||||
export async function getGuildTemplates(guildID: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
|
||||
const templates = await RequestManager.get(
|
||||
@@ -708,7 +723,7 @@ export async function deleteGuildTemplate(
|
||||
guildID: string,
|
||||
templateCode: string,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
|
||||
const deletedTemplate = await RequestManager.delete(
|
||||
@@ -727,7 +742,7 @@ export async function createGuildTemplate(
|
||||
guildID: string,
|
||||
data: CreateGuildTemplate,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
|
||||
if (data.name.length < 1 || data.name.length > 100) {
|
||||
@@ -753,7 +768,7 @@ export async function createGuildTemplate(
|
||||
* Requires the `MANAGE_GUILD` permission.
|
||||
*/
|
||||
export async function syncGuildTemplate(guildID: string, templateCode: string) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
|
||||
const template = await RequestManager.put(
|
||||
@@ -771,7 +786,7 @@ export async function editGuildTemplate(
|
||||
templateCode: string,
|
||||
data: EditGuildTemplate,
|
||||
) {
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]);
|
||||
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
|
||||
|
||||
if (data.name?.length && (data.name.length < 1 || data.name.length > 100)) {
|
||||
@@ -1,21 +1,22 @@
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { botID } from "../module/client.ts";
|
||||
import { RequestManager } from "../module/requestManager.ts";
|
||||
import { Member } from "../structures/member.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { ImageFormats, ImageSize } from "../types/cdn.ts";
|
||||
import { DMChannelCreatePayload, MessageContent } from "../types/channel.ts";
|
||||
import { Errors } from "../types/errors.ts";
|
||||
import { EditMemberOptions } from "../types/member.ts";
|
||||
import { Permissions } from "../types/permission.ts";
|
||||
import { formatImageURL } from "../utils/cdn.ts";
|
||||
import { endpoints } from "../utils/constants.ts";
|
||||
import { botID } from "../../bot.ts";
|
||||
import { RequestManager } from "../../rest/mod.ts";
|
||||
import {
|
||||
DMChannelCreatePayload,
|
||||
EditMemberOptions,
|
||||
Errors,
|
||||
ImageFormats,
|
||||
ImageSize,
|
||||
MessageContent,
|
||||
} from "../../types/mod.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import {
|
||||
botHasPermission,
|
||||
higherRolePosition,
|
||||
highestRole,
|
||||
} from "../utils/permissions.ts";
|
||||
import { urlToBase64 } from "../utils/utils.ts";
|
||||
} from "../../util/permissions.ts";
|
||||
import { formatImageURL, urlToBase64 } from "../../util/utils.ts";
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { Member, structures } from "../structures/structures.ts";
|
||||
import { sendMessage } from "./channel.ts";
|
||||
|
||||
/** The users custom avatar or the default avatar if you don't have a member object. */
|
||||
@@ -38,9 +39,9 @@ export function avatarURL(
|
||||
format?: ImageFormats,
|
||||
) {
|
||||
return rawAvatarURL(
|
||||
member.user.id,
|
||||
member.user.discriminator,
|
||||
member.user.avatar,
|
||||
member.id,
|
||||
member.discriminator,
|
||||
member.avatar,
|
||||
size,
|
||||
format,
|
||||
);
|
||||
@@ -60,12 +61,15 @@ export async function addRole(
|
||||
botsHighestRole.id,
|
||||
roleID,
|
||||
);
|
||||
if (!hasHigherRolePosition) {
|
||||
if (
|
||||
!hasHigherRolePosition &&
|
||||
(await cacheHandlers.get("guilds", guildID))?.ownerID !== botID
|
||||
) {
|
||||
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
@@ -91,12 +95,15 @@ export async function removeRole(
|
||||
botsHighestRole.id,
|
||||
roleID,
|
||||
);
|
||||
if (!hasHigherRolePosition) {
|
||||
if (
|
||||
!hasHigherRolePosition &&
|
||||
(await cacheHandlers.get("guilds", guildID))?.ownerID !== botID
|
||||
) {
|
||||
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
|
||||
const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_ROLES);
|
||||
}
|
||||
@@ -142,7 +149,7 @@ export async function kick(guildID: string, memberID: string, reason?: string) {
|
||||
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
|
||||
}
|
||||
|
||||
const hasPerm = await botHasPermission(guildID, [Permissions.KICK_MEMBERS]);
|
||||
const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]);
|
||||
if (!hasPerm) {
|
||||
throw new Error(Errors.MISSING_KICK_MEMBERS);
|
||||
}
|
||||
@@ -166,7 +173,7 @@ export async function editMember(
|
||||
|
||||
const hasManageNickPerm = await botHasPermission(
|
||||
guildID,
|
||||
[Permissions.MANAGE_NICKNAMES],
|
||||
["MANAGE_NICKNAMES"],
|
||||
);
|
||||
if (!hasManageNickPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_NICKNAMES);
|
||||
@@ -175,7 +182,7 @@ export async function editMember(
|
||||
|
||||
const hasManageRolesPerm = await botHasPermission(
|
||||
guildID,
|
||||
[Permissions.MANAGE_ROLES],
|
||||
["MANAGE_ROLES"],
|
||||
);
|
||||
if (
|
||||
options.roles &&
|
||||
@@ -187,7 +194,7 @@ export async function editMember(
|
||||
if (options.mute) {
|
||||
const hasMuteMembersPerm = await botHasPermission(
|
||||
guildID,
|
||||
[Permissions.MUTE_MEMBERS],
|
||||
["MUTE_MEMBERS"],
|
||||
);
|
||||
// TODO: This should check if the member is in a voice channel
|
||||
if (
|
||||
@@ -199,7 +206,7 @@ export async function editMember(
|
||||
|
||||
const hasDeafenMembersPerm = await botHasPermission(
|
||||
guildID,
|
||||
[Permissions.DEAFEN_MEMBERS],
|
||||
["DEAFEN_MEMBERS"],
|
||||
);
|
||||
if (
|
||||
options.deaf &&
|
||||
@@ -1,16 +1,16 @@
|
||||
import { delay } from "../../deps.ts";
|
||||
import { botID } from "../../bot.ts";
|
||||
import { RequestManager } from "../../rest/mod.ts";
|
||||
import {
|
||||
Errors,
|
||||
MessageContent,
|
||||
MessageCreateOptions,
|
||||
UserPayload,
|
||||
} from "../../types/mod.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import { botHasChannelPermissions } from "../../util/permissions.ts";
|
||||
import { delay } from "../../util/utils.ts";
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { botID } from "../module/client.ts";
|
||||
import { RequestManager } from "../module/requestManager.ts";
|
||||
import { Message } from "../structures/message.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { MessageContent } from "../types/channel.ts";
|
||||
import { Errors } from "../types/errors.ts";
|
||||
import { UserPayload } from "../types/guild.ts";
|
||||
import { MessageCreateOptions } from "../types/message.ts";
|
||||
import { Permissions } from "../types/permission.ts";
|
||||
import { endpoints } from "../utils/constants.ts";
|
||||
import { botHasChannelPermissions } from "../utils/permissions.ts";
|
||||
import { Message, structures } from "../structures/structures.ts";
|
||||
|
||||
/** Delete a message with the channel id and message id only. */
|
||||
export async function deleteMessageByID(
|
||||
@@ -40,7 +40,7 @@ export async function deleteMessage(
|
||||
// This needs to check the channels permission not the guild permission
|
||||
const hasManageMessages = await botHasChannelPermissions(
|
||||
message.channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasManageMessages
|
||||
@@ -61,7 +61,7 @@ export async function deleteMessage(
|
||||
export async function pin(channelID: string, messageID: string) {
|
||||
const hasManageMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasManageMessagesPerm
|
||||
@@ -75,7 +75,7 @@ export async function pin(channelID: string, messageID: string) {
|
||||
export async function unpin(channelID: string, messageID: string) {
|
||||
const hasManageMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasManageMessagesPerm
|
||||
@@ -95,7 +95,7 @@ export async function addReaction(
|
||||
) {
|
||||
const hasAddReactionsPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.ADD_REACTIONS],
|
||||
["ADD_REACTIONS"],
|
||||
);
|
||||
if (!hasAddReactionsPerm) {
|
||||
throw new Error(Errors.MISSING_ADD_REACTIONS);
|
||||
@@ -103,7 +103,7 @@ export async function addReaction(
|
||||
|
||||
const hasReadMessageHistoryPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.READ_MESSAGE_HISTORY],
|
||||
["READ_MESSAGE_HISTORY"],
|
||||
);
|
||||
if (
|
||||
!hasReadMessageHistoryPerm
|
||||
@@ -168,7 +168,7 @@ export async function removeUserReaction(
|
||||
) {
|
||||
const hasManageMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (!hasManageMessagesPerm) {
|
||||
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
|
||||
@@ -188,7 +188,7 @@ export async function removeUserReaction(
|
||||
export async function removeAllReactions(channelID: string, messageID: string) {
|
||||
const hasManageMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasManageMessagesPerm
|
||||
@@ -208,7 +208,7 @@ export async function removeReactionEmoji(
|
||||
) {
|
||||
const hasManageMessagesPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_MESSAGES],
|
||||
["MANAGE_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasManageMessagesPerm
|
||||
@@ -225,11 +225,11 @@ export async function getReactions(message: Message, reaction: string) {
|
||||
const result = (await RequestManager.get(
|
||||
endpoints.CHANNEL_MESSAGE_REACTION(message.channelID, message.id, reaction),
|
||||
)) as UserPayload[];
|
||||
const guild = await cacheHandlers.get("guilds", message.guildID);
|
||||
|
||||
return result.map((res) => {
|
||||
return guild?.members.get(res.id) || res;
|
||||
});
|
||||
return Promise.all(result.map(async (res) => {
|
||||
const member = await cacheHandlers.get("members", res.id);
|
||||
return member || res;
|
||||
}));
|
||||
}
|
||||
|
||||
/** Edit the message. */
|
||||
@@ -247,7 +247,7 @@ export async function editMessage(
|
||||
|
||||
const hasSendMessagesPerm = await botHasChannelPermissions(
|
||||
message.channelID,
|
||||
[Permissions.SEND_MESSAGES],
|
||||
["SEND_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
!hasSendMessagesPerm
|
||||
@@ -257,7 +257,7 @@ export async function editMessage(
|
||||
|
||||
const hasSendTtsMessagesPerm = await botHasChannelPermissions(
|
||||
message.channelID,
|
||||
[Permissions.SEND_TTS_MESSAGES],
|
||||
["SEND_TTS_MESSAGES"],
|
||||
);
|
||||
if (
|
||||
content.tts &&
|
||||
316
src/api/handlers/webhook.ts
Normal file
316
src/api/handlers/webhook.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { botID } from "../../bot.ts";
|
||||
import { RequestManager } from "../../rest/mod.ts";
|
||||
import {
|
||||
CreateSlashCommandOptions,
|
||||
EditSlashCommandOptions,
|
||||
EditSlashResponseOptions,
|
||||
EditWebhookMessageOptions,
|
||||
Errors,
|
||||
ExecuteSlashCommandOptions,
|
||||
ExecuteWebhookOptions,
|
||||
MessageCreateOptions,
|
||||
UpsertSlashCommandOptions,
|
||||
WebhookCreateOptions,
|
||||
WebhookPayload,
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import { botHasChannelPermissions } from "../../util/permissions.ts";
|
||||
import { urlToBase64 } from "../../util/utils.ts";
|
||||
import { structures } from "../structures/structures.ts";
|
||||
|
||||
/** Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations:
|
||||
*
|
||||
* Webhook names cannot be: 'clyde'
|
||||
*/
|
||||
export async function createWebhook(
|
||||
channelID: string,
|
||||
options: WebhookCreateOptions,
|
||||
) {
|
||||
const hasManageWebhooksPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
["MANAGE_WEBHOOKS"],
|
||||
);
|
||||
if (
|
||||
!hasManageWebhooksPerm
|
||||
) {
|
||||
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
|
||||
}
|
||||
|
||||
if (
|
||||
// Specific usernames that discord does not allow
|
||||
options.name === "clyde" ||
|
||||
// Character limit checks. [...] checks are because of js unicode length handling
|
||||
[...options.name].length < 2 || [...options.name].length > 32
|
||||
) {
|
||||
throw new Error(Errors.INVALID_WEBHOOK_NAME);
|
||||
}
|
||||
|
||||
return RequestManager.post(
|
||||
endpoints.CHANNEL_WEBHOOKS(channelID),
|
||||
{
|
||||
...options,
|
||||
avatar: options.avatar ? await urlToBase64(options.avatar) : undefined,
|
||||
},
|
||||
) as Promise<WebhookPayload>;
|
||||
}
|
||||
|
||||
export async function executeWebhook(
|
||||
webhookID: string,
|
||||
webhookToken: string,
|
||||
options: ExecuteWebhookOptions,
|
||||
) {
|
||||
if (!options.content && !options.file && !options.embeds) {
|
||||
throw new Error(Errors.INVALID_WEBHOOK_OPTIONS);
|
||||
}
|
||||
|
||||
if (options.content && options.content.length > 2000) {
|
||||
throw Error(Errors.MESSAGE_MAX_LENGTH);
|
||||
}
|
||||
|
||||
if (options.embeds && options.embeds.length > 10) {
|
||||
options.embeds.splice(10);
|
||||
}
|
||||
|
||||
if (options.mentions) {
|
||||
if (options.mentions.users?.length) {
|
||||
if (options.mentions.parse.includes("users")) {
|
||||
options.mentions.parse = options.mentions.parse.filter((p) =>
|
||||
p !== "users"
|
||||
);
|
||||
}
|
||||
|
||||
if (options.mentions.users.length > 100) {
|
||||
options.mentions.users = options.mentions.users.slice(0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.mentions.roles?.length) {
|
||||
if (options.mentions.parse.includes("roles")) {
|
||||
options.mentions.parse = options.mentions.parse.filter((p) =>
|
||||
p !== "roles"
|
||||
);
|
||||
}
|
||||
|
||||
if (options.mentions.roles.length > 100) {
|
||||
options.mentions.roles = options.mentions.roles.slice(0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await RequestManager.post(
|
||||
`${endpoints.WEBHOOK(webhookID, webhookToken)}${
|
||||
options.wait ? "?wait=true" : ""
|
||||
}`,
|
||||
{
|
||||
...options,
|
||||
allowed_mentions: options.mentions,
|
||||
avatar_url: options.avatar_url,
|
||||
},
|
||||
);
|
||||
if (!options.wait) return;
|
||||
|
||||
return structures.createMessage(result as MessageCreateOptions);
|
||||
}
|
||||
|
||||
export function getWebhook(webhookID: string) {
|
||||
return RequestManager.get(endpoints.WEBHOOK_ID(webhookID));
|
||||
}
|
||||
|
||||
export function editWebhookMessage(
|
||||
webhookID: string,
|
||||
webhookToken: string,
|
||||
messageID: string,
|
||||
options: EditWebhookMessageOptions,
|
||||
) {
|
||||
if (options.content && options.content.length > 2000) {
|
||||
throw Error(Errors.MESSAGE_MAX_LENGTH);
|
||||
}
|
||||
|
||||
if (options.embeds && options.embeds.length > 10) {
|
||||
options.embeds.splice(10);
|
||||
}
|
||||
|
||||
if (options.allowed_mentions) {
|
||||
if (options.allowed_mentions.users?.length) {
|
||||
if (options.allowed_mentions.parse.includes("users")) {
|
||||
options.allowed_mentions.parse = options.allowed_mentions.parse.filter((
|
||||
p,
|
||||
) => p !== "users");
|
||||
}
|
||||
|
||||
if (options.allowed_mentions.users.length > 100) {
|
||||
options.allowed_mentions.users = options.allowed_mentions.users.slice(
|
||||
0,
|
||||
100,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.allowed_mentions.roles?.length) {
|
||||
if (options.allowed_mentions.parse.includes("roles")) {
|
||||
options.allowed_mentions.parse = options.allowed_mentions.parse.filter((
|
||||
p,
|
||||
) => p !== "roles");
|
||||
}
|
||||
|
||||
if (options.allowed_mentions.roles.length > 100) {
|
||||
options.allowed_mentions.roles = options.allowed_mentions.roles.slice(
|
||||
0,
|
||||
100,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RequestManager.patch(
|
||||
endpoints.WEBHOOK_EDIT(webhookID, webhookToken, messageID),
|
||||
{ ...options, allowed_mentions: options.allowed_mentions },
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteWebhookMessage(
|
||||
webhookID: string,
|
||||
webhookToken: string,
|
||||
messageID: string,
|
||||
) {
|
||||
return RequestManager.delete(
|
||||
endpoints.WEBHOOK_DELETE(webhookID, webhookToken, messageID),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* There are two kinds of Slash Commands: global commands and guild commands. Global commands are available for every guild that adds your app; guild commands are specific to the guild you specify when making them. Command names are unique per application within each scope (global and guild). That means:
|
||||
*
|
||||
* - Your app **cannot** have two global commands with the same name
|
||||
* - Your app **cannot** have two guild commands within the same name **on the same guild**
|
||||
* - Your app **can** have a global and guild command with the same name
|
||||
* - Multiple apps **can** have commands with the same names
|
||||
*
|
||||
* Global commands are cached for **1 hour**. That means that new global commands will fan out slowly across all guilds, and will be guaranteed to be updated in an hour.
|
||||
* Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use.
|
||||
*/
|
||||
export function createSlashCommand(options: CreateSlashCommandOptions) {
|
||||
// Use ... for content length due to unicode characters and js .length handling
|
||||
if ([...options.name].length < 2 || [...options.name].length > 32) {
|
||||
throw new Error(Errors.INVALID_SLASH_NAME);
|
||||
}
|
||||
|
||||
if (
|
||||
[...options.description].length < 1 || [...options.description].length > 100
|
||||
) {
|
||||
throw new Error(Errors.INVALID_SLASH_DESCRIPTION);
|
||||
}
|
||||
|
||||
return RequestManager.post(
|
||||
options.guildID
|
||||
? endpoints.COMMANDS_GUILD(botID, options.guildID)
|
||||
: endpoints.COMMANDS(botID),
|
||||
{
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch all of the global commands for your application. */
|
||||
export function getSlashCommands(guildID?: string) {
|
||||
// TODO: Should this be a returned as a collection?
|
||||
return RequestManager.get(
|
||||
guildID
|
||||
? endpoints.COMMANDS_GUILD(botID, guildID)
|
||||
: endpoints.COMMANDS(botID),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an existing slash command. If this command did not exist, it will create it.
|
||||
*/
|
||||
export function upsertSlashCommand(options: UpsertSlashCommandOptions) {
|
||||
return RequestManager.post(
|
||||
options.guildID
|
||||
? endpoints.COMMANDS_GUILD_ID(botID, options.id, options.guildID)
|
||||
: endpoints.COMMANDS_ID(botID, options.id),
|
||||
{
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Edit an existing slash command. */
|
||||
export function editSlashCommand(options: EditSlashCommandOptions) {
|
||||
return RequestManager.patch(
|
||||
options.guildID
|
||||
? endpoints.COMMANDS_GUILD_ID(botID, options.id, options.guildID)
|
||||
: endpoints.COMMANDS_ID(botID, options.id),
|
||||
{
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Deletes a slash command. */
|
||||
export function deleteSlashCommand(id: string, guildID?: string) {
|
||||
if (!guildID) return RequestManager.delete(endpoints.COMMANDS_ID(botID, id));
|
||||
return RequestManager.delete(endpoints.COMMANDS_GUILD_ID(botID, id, guildID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a response to a users slash command. The command data will have the id and token necessary to respond.
|
||||
* Interaction `tokens` are valid for **15 minutes** and can be used to send followup messages.
|
||||
*
|
||||
* NOTE: By default we will suppress mentions. To enable mentions, just pass any mentions object.
|
||||
*/
|
||||
export function executeSlashCommand(
|
||||
id: string,
|
||||
token: string,
|
||||
options: ExecuteSlashCommandOptions,
|
||||
) {
|
||||
// If its already been executed, we need to send a followup response
|
||||
if (cache.executedSlashCommands.has(token)) {
|
||||
return RequestManager.post(endpoints.WEBHOOK(botID, token), {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Expire in 15 minutes
|
||||
cache.executedSlashCommands.set(token, id);
|
||||
setTimeout(
|
||||
() => cache.executedSlashCommands.delete(token),
|
||||
Date.now() + 900000,
|
||||
);
|
||||
|
||||
// IF NO MENTIONS ARE PROVIDED, FORCE DISABLE MENTIONS
|
||||
if (!(options.data.allowed_mentions)) {
|
||||
options.data.allowed_mentions = { parse: [] };
|
||||
}
|
||||
|
||||
return RequestManager.post(endpoints.INTERACTION_ID_TOKEN(id, token), {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/** To delete your response to a slash command. If a message id is not provided, it will default to deleting the original response. */
|
||||
export function deleteSlashResponse(
|
||||
token: string,
|
||||
messageID?: string,
|
||||
) {
|
||||
if (!messageID) {
|
||||
return RequestManager.delete(
|
||||
endpoints.INTERACTION_ORIGINAL_ID_TOKEN(botID, token),
|
||||
);
|
||||
}
|
||||
return RequestManager.delete(
|
||||
endpoints.INTERACTION_ID_TOKEN_MESSAGEID(botID, token, messageID),
|
||||
);
|
||||
}
|
||||
|
||||
/** To edit your response to a slash command. If a messageID is not provided it will default to editing the original response. */
|
||||
export function editSlashResponse(
|
||||
token: string,
|
||||
options: EditSlashResponseOptions,
|
||||
) {
|
||||
return RequestManager.patch(
|
||||
endpoints.INTERACTION_ORIGINAL_ID_TOKEN(botID, token),
|
||||
options,
|
||||
);
|
||||
}
|
||||
120
src/api/structures/channel.ts
Normal file
120
src/api/structures/channel.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
ChannelCreatePayload,
|
||||
ChannelType,
|
||||
MessageContent,
|
||||
RawOverwrite,
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { createNewProp } from "../../util/utils.ts";
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { sendMessage } from "../handlers/channel.ts";
|
||||
import { Guild } from "./guild.ts";
|
||||
import { Message } from "./message.ts";
|
||||
|
||||
const baseChannel: Partial<Channel> = {
|
||||
get guild() {
|
||||
return cache.guilds.get(this.guildID!);
|
||||
},
|
||||
get messages() {
|
||||
return cache.messages.filter((m) => m.channelID === this.id!);
|
||||
},
|
||||
get mention() {
|
||||
return `<#${this.id!}>`;
|
||||
},
|
||||
send(content) {
|
||||
return sendMessage(this.id!, content);
|
||||
},
|
||||
};
|
||||
|
||||
export async function createChannel(
|
||||
data: ChannelCreatePayload,
|
||||
guildID?: string,
|
||||
) {
|
||||
const {
|
||||
guild_id: rawGuildID,
|
||||
last_message_id: lastMessageID,
|
||||
user_limit: userLimit,
|
||||
rate_limit_per_user: rateLimitPerUser,
|
||||
parent_id: parentID,
|
||||
last_pin_timestamp: lastPinTimestamp,
|
||||
permission_overwrites,
|
||||
nsfw,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const restProps: Record<string, ReturnType<typeof createNewProp>> = {};
|
||||
for (const key of Object.keys(rest)) {
|
||||
restProps[key] = createNewProp((rest as any)[key]);
|
||||
}
|
||||
|
||||
const channel = Object.create(baseChannel, {
|
||||
...restProps,
|
||||
guildID: createNewProp(guildID || rawGuildID || ""),
|
||||
lastMessageID: createNewProp(lastMessageID),
|
||||
userLimit: createNewProp(userLimit),
|
||||
rateLimitPerUser: createNewProp(rateLimitPerUser),
|
||||
parentID: createNewProp(parentID || undefined),
|
||||
lastPinTimestamp: createNewProp(
|
||||
lastPinTimestamp ? Date.parse(lastPinTimestamp) : undefined,
|
||||
),
|
||||
permissionOverwrites: createNewProp(permission_overwrites || []),
|
||||
nsfw: createNewProp(data.nsfw || false),
|
||||
});
|
||||
|
||||
cacheHandlers.set("channels", data.id, channel);
|
||||
return channel as Channel;
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
/** The id of this channel */
|
||||
id: string;
|
||||
/** Sorting position of the channel */
|
||||
position?: number;
|
||||
/** The name of the channel (2-100 characters) */
|
||||
name?: string;
|
||||
/** The channel topic (0-1024 characters) */
|
||||
topic?: string;
|
||||
/** The bitrate (in bits) of the voice channel */
|
||||
bitrate?: number;
|
||||
/** The type of the channel */
|
||||
type: ChannelType;
|
||||
/** The guild id of the channel if it is a guild channel. */
|
||||
guildID: string;
|
||||
/** The id of the last message sent in this channel */
|
||||
lastMessageID?: string;
|
||||
/** The amount of users allowed in this voice channel. */
|
||||
userLimit?: number;
|
||||
/** The rate limit(slowmode) in this text channel that users can send messages. */
|
||||
rateLimitPerUser?: number;
|
||||
/** The category id for this channel */
|
||||
parentID?: string;
|
||||
/** The last time when a message was pinned in this channel */
|
||||
lastPinTimestamp?: number;
|
||||
/** The permission overwrites for this channel */
|
||||
permissionOverwrites: RawOverwrite[];
|
||||
/** Whether this channel is nsfw or not */
|
||||
nsfw: boolean;
|
||||
|
||||
// GETTERS
|
||||
|
||||
/**
|
||||
* Gets the guild object for this channel.
|
||||
*
|
||||
* ⚠️ ADVANCED: If you use the custom cache, these will not work for you. Getters can not be async and custom cache requires async.
|
||||
*/
|
||||
guild?: Guild;
|
||||
/**
|
||||
* Gets the messages from cache that were sent in this channel
|
||||
*
|
||||
* ⚠️ ADVANCED: If you use the custom cache, these will not work for you. Getters can not be async and custom cache requires async.
|
||||
*/
|
||||
messages: Collection<string, Message>;
|
||||
/** The mention of the channel */
|
||||
mention: string;
|
||||
|
||||
// METHODS
|
||||
|
||||
/** Send a message to the channel. Requires SEND_MESSAGES permission. */
|
||||
send(content: string | MessageContent): Promise<Message>;
|
||||
}
|
||||
365
src/api/structures/guild.ts
Normal file
365
src/api/structures/guild.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import { botID } from "../../bot.ts";
|
||||
import {
|
||||
BannedUser,
|
||||
BanOptions,
|
||||
ChannelCreatePayload,
|
||||
CreateGuildPayload,
|
||||
Emoji,
|
||||
GetAuditLogsOptions,
|
||||
GuildEditOptions,
|
||||
GuildFeatures,
|
||||
GuildMember,
|
||||
ImageFormats,
|
||||
ImageSize,
|
||||
MemberCreatePayload,
|
||||
Presence,
|
||||
RoleData,
|
||||
VoiceState,
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { createNewProp } from "../../util/utils.ts";
|
||||
import {
|
||||
ban,
|
||||
deleteServer,
|
||||
editGuild,
|
||||
getAuditLogs,
|
||||
getBan,
|
||||
getBans,
|
||||
getInvites,
|
||||
guildBannerURL,
|
||||
guildIconURL,
|
||||
leaveGuild,
|
||||
unban,
|
||||
} from "../handlers/guild.ts";
|
||||
import { Member } from "./member.ts";
|
||||
import { Role, structures } from "./mod.ts";
|
||||
import { Channel } from "./structures.ts";
|
||||
|
||||
export const initialMemberLoadQueue = new Map<string, MemberCreatePayload[]>();
|
||||
|
||||
const baseGuild: Partial<Guild> = {
|
||||
get members() {
|
||||
return cache.members.filter((member) => member.guilds.has(this.id!));
|
||||
},
|
||||
get channels() {
|
||||
return cache.channels.filter((channel) => channel.guildID === this.id);
|
||||
},
|
||||
get afkChannel() {
|
||||
return cache.channels.get(this.afkChannelID!);
|
||||
},
|
||||
get publicUpdatesChannel() {
|
||||
return cache.channels.get(this.publicUpdatesChannelID!);
|
||||
},
|
||||
get rulesChannel() {
|
||||
return cache.channels.get(this.rulesChannelID!);
|
||||
},
|
||||
get systemChannel() {
|
||||
return cache.channels.get(this.systemChannelID!);
|
||||
},
|
||||
get bot() {
|
||||
return cache.members.get(botID);
|
||||
},
|
||||
get botMember() {
|
||||
return this.bot?.guilds.get(this.id!);
|
||||
},
|
||||
get botVoice() {
|
||||
return this.voiceStates?.get(botID);
|
||||
},
|
||||
get owner() {
|
||||
return cache.members.get(this.ownerID!);
|
||||
},
|
||||
get partnered() {
|
||||
return Boolean(this.features?.includes("PARTNERED"));
|
||||
},
|
||||
get verified() {
|
||||
return Boolean(this.features?.includes("VERIFIED"));
|
||||
},
|
||||
bannerURL(size, format) {
|
||||
return guildBannerURL(this as Guild, size, format);
|
||||
},
|
||||
delete() {
|
||||
return deleteServer(this.id!);
|
||||
},
|
||||
edit(options) {
|
||||
return editGuild(this.id!, options);
|
||||
},
|
||||
auditLogs(options) {
|
||||
return getAuditLogs(this.id!, options);
|
||||
},
|
||||
getBan(memberID) {
|
||||
return getBan(this.id!, memberID);
|
||||
},
|
||||
bans() {
|
||||
return getBans(this.id!);
|
||||
},
|
||||
ban(memberID, options) {
|
||||
return ban(this.id!, memberID, options);
|
||||
},
|
||||
unban(memberID) {
|
||||
return unban(this.id!, memberID);
|
||||
},
|
||||
invites() {
|
||||
return getInvites(this.id!);
|
||||
},
|
||||
iconURL(size, format) {
|
||||
return guildIconURL(this as Guild, size, format);
|
||||
},
|
||||
leave() {
|
||||
return leaveGuild(this.id!);
|
||||
},
|
||||
};
|
||||
|
||||
export async function createGuild(data: CreateGuildPayload, shardID: number) {
|
||||
const {
|
||||
disovery_splash: discoverySplash,
|
||||
default_message_notifications: defaultMessageNotifications,
|
||||
explicit_content_filter: explicitContentFilter,
|
||||
system_channel_flags: systemChannelFlags,
|
||||
rules_channel_id: rulesChannelID,
|
||||
public_updates_channel_id: publicUpdatesChannelID,
|
||||
max_video_channel_users: maxVideoChannelUsers,
|
||||
approximate_member_count: approximateMemberCount,
|
||||
approximate_presence_count: approximatePresenceCount,
|
||||
owner_id: ownerID,
|
||||
afk_channel_id: afkChannelID,
|
||||
afk_timeout: afkTimeout,
|
||||
widget_enabled: widgetEnabled,
|
||||
widget_channel_id: widgetChannelID,
|
||||
verification_level: verificationLevel,
|
||||
mfa_level: mfaLevel,
|
||||
system_channel_id: systemChannelID,
|
||||
max_presences: maxPresences,
|
||||
max_members: maxMembers,
|
||||
vanity_url_code: vanityURLCode,
|
||||
premium_tier: premiumTier,
|
||||
premium_subscription_count: premiumSubscriptionCount,
|
||||
preferred_locale: preferredLocale,
|
||||
joined_at: joinedAt,
|
||||
member_count: memberCount,
|
||||
voice_states: voiceStates,
|
||||
channels,
|
||||
members,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const roles = (await Promise.all(
|
||||
data.roles.map((r: RoleData) => structures.createRole(r)),
|
||||
)) as Role[];
|
||||
|
||||
await Promise.all(
|
||||
channels.map((c: ChannelCreatePayload) =>
|
||||
structures.createChannel(c, data.id)
|
||||
),
|
||||
);
|
||||
|
||||
const restProps: Record<string, ReturnType<typeof createNewProp>> = {};
|
||||
for (const key of Object.keys(rest)) {
|
||||
restProps[key] = createNewProp((rest as any)[key]);
|
||||
}
|
||||
|
||||
const guild = Object.create(baseGuild, {
|
||||
...restProps,
|
||||
discoverySplash: createNewProp(discoverySplash),
|
||||
defaultMessageNotifications: createNewProp(defaultMessageNotifications),
|
||||
explicitContentFilter: createNewProp(explicitContentFilter),
|
||||
rulesChannelID: createNewProp(rulesChannelID),
|
||||
publicUpdatesChannelID: createNewProp(publicUpdatesChannelID),
|
||||
maxVideoChannelUsers: createNewProp(maxVideoChannelUsers),
|
||||
approximateMemberCount: createNewProp(approximateMemberCount),
|
||||
approximatePresenceCount: createNewProp(approximatePresenceCount),
|
||||
shardID: createNewProp(shardID),
|
||||
ownerID: createNewProp(ownerID),
|
||||
afkChannelID: createNewProp(afkChannelID),
|
||||
afkTimeout: createNewProp(afkTimeout),
|
||||
widgetEnabled: createNewProp(widgetEnabled),
|
||||
widgetChannelID: createNewProp(widgetChannelID),
|
||||
verificationLevel: createNewProp(verificationLevel),
|
||||
mfaLevel: createNewProp(mfaLevel),
|
||||
systemChannelID: createNewProp(systemChannelID),
|
||||
maxPresences: createNewProp(maxPresences),
|
||||
maxMembers: createNewProp(maxMembers),
|
||||
vanityURLCode: createNewProp(vanityURLCode),
|
||||
premiumTier: createNewProp(premiumTier),
|
||||
premiumSubscriptionCount: createNewProp(premiumSubscriptionCount),
|
||||
preferredLocale: createNewProp(preferredLocale),
|
||||
roles: createNewProp(new Collection(roles.map((r: Role) => [r.id, r]))),
|
||||
joinedAt: createNewProp(Date.parse(joinedAt)),
|
||||
presences: createNewProp(
|
||||
new Collection(data.presences.map((p: Presence) => [p.user.id, p])),
|
||||
),
|
||||
memberCount: createNewProp(memberCount || 0),
|
||||
voiceStates: createNewProp(
|
||||
new Collection(
|
||||
voiceStates.map((vs: VoiceState) => [
|
||||
vs.user_id,
|
||||
{
|
||||
...vs,
|
||||
guildID: vs.guild_id,
|
||||
channelID: vs.channel_id,
|
||||
userID: vs.user_id,
|
||||
sessionID: vs.session_id,
|
||||
selfDeaf: vs.self_deaf,
|
||||
selfMute: vs.self_mute,
|
||||
selfStream: vs.self_stream,
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
initialMemberLoadQueue.set(guild.id, members);
|
||||
|
||||
return guild;
|
||||
}
|
||||
|
||||
export interface Guild {
|
||||
/** The guild id */
|
||||
id: string;
|
||||
/** The guild name 2-100 characters */
|
||||
name: string;
|
||||
/** The guild icon image hash */
|
||||
icon: string | null;
|
||||
/** The guild splash image hash */
|
||||
splash: string | null;
|
||||
/** Discovery splash has; only present for guilds with the "DISCOVERABLE" feature */
|
||||
disoverySplash: string | null;
|
||||
/** The voice region id for the guild */
|
||||
region: string;
|
||||
/** Default message notifications level */
|
||||
defaultMessageNotifications: number;
|
||||
/** Explicit content filter level */
|
||||
explicitContentFilter: number;
|
||||
/** The custom guild emojis */
|
||||
emojis: Emoji[];
|
||||
/** Enabled guild features */
|
||||
features: GuildFeatures[];
|
||||
/** System channel flags */
|
||||
systemChannelFlags: number;
|
||||
/** The id of the channel where guilds with the PUBLIC feature can display rules and or guidelines. */
|
||||
rulesChannelID: string | null;
|
||||
/** The description for the guild */
|
||||
description: string | null;
|
||||
/** The banner hash */
|
||||
banner: string | null;
|
||||
/** The id of the channel where admins and moderators of guilds with the PUBLIC feature receive notices from Discord */
|
||||
publicUpdatesChannelID: string | null;
|
||||
/** The maximum amount of users in a video channel. */
|
||||
maxVideoChannelUsers?: number;
|
||||
/** The approximate number of members in this guild, returned from the GET /guild/id endpoint when with_counts is true */
|
||||
approximateMemberCount?: number;
|
||||
/** The approximate number of non-offline members in this guild, returned from the GET /guild/id endpoint when with_counts is true */
|
||||
approximatePresenceCount?: number;
|
||||
/** Whether this is considered a large guild */
|
||||
large: boolean;
|
||||
/** Whether this guild is unavailable */
|
||||
unavailable: boolean;
|
||||
/** The shard id that this guild is on */
|
||||
shardID: number;
|
||||
/** The owner id of the guild. */
|
||||
ownerID: string;
|
||||
/** The afk channel id for this guild. */
|
||||
afkChannelID: string;
|
||||
/** The amount of time before a user is moved to AFK. */
|
||||
afkTimeout: number;
|
||||
/** Whether or not the embed is enabled in this server. */
|
||||
widgetEnabled: boolean;
|
||||
/** The channel id for the guild embed in this server. */
|
||||
widgetChannelID: string;
|
||||
/** The verification level for this server. */
|
||||
verificationLevel: number;
|
||||
/** The MFA level for this server. */
|
||||
mfaLevel: number;
|
||||
/** The system channel id for this server. */
|
||||
systemChannelID: string;
|
||||
/** The max presences for this server. */
|
||||
maxPresences: number;
|
||||
/** The maximum members in this server. */
|
||||
maxMembers: number;
|
||||
/** The vanity URL code for this server. */
|
||||
vanityURLCode: string;
|
||||
/** The premium tier for this server. */
|
||||
premiumTier: number;
|
||||
/** The subscription count for this server. */
|
||||
premiumSubscriptionCount: number;
|
||||
/** The preferred language in this server. */
|
||||
preferredLocale: string;
|
||||
/** The roles in the guild */
|
||||
roles: Collection<string, Role>;
|
||||
/** When this guild was joined at. */
|
||||
joinedAt: number;
|
||||
/** The presences of all the users in the guild. */
|
||||
presences: Collection<string, Presence>;
|
||||
/** The total number of members in this guild. This value is updated as members leave and join the server. However, if you do not have the intent enabled to be able to listen to these events, then this will not be accurate. */
|
||||
memberCount: number;
|
||||
/** The Voice State data for each user in a voice channel in this server. */
|
||||
voiceStates: Collection<string, CleanVoiceState>;
|
||||
|
||||
// GETTERS
|
||||
/** Members in this guild. */
|
||||
members: Collection<string, Member>;
|
||||
/** Channels in this guild. */
|
||||
channels: Collection<string, Channel>;
|
||||
/** The afk channel if one is set */
|
||||
afkChannel?: Channel;
|
||||
/** The public update channel if one is set */
|
||||
publicUpdatesChannel?: Channel;
|
||||
/** The rules channel in this guild if one is set */
|
||||
rulesChannel?: Channel;
|
||||
/** The system channel in this guild if one is set */
|
||||
systemChannel?: Channel;
|
||||
/** The bot member in this guild if cached */
|
||||
bot?: Member;
|
||||
/** The bot guild member in this guild if cached */
|
||||
botMember?: GuildMember;
|
||||
/** The bots voice state if there is one in this guild */
|
||||
botVoice?: CleanVoiceState;
|
||||
/** The owner member of this guild */
|
||||
owner?: Member;
|
||||
/** Whether or not this guild is partnered */
|
||||
partnered: boolean;
|
||||
/** Whether or not this guild is verified */
|
||||
verified: boolean;
|
||||
|
||||
// METHODS
|
||||
|
||||
/** The banner url for this server */
|
||||
bannerURL(size?: ImageSize, format?: ImageFormats): string | undefined;
|
||||
/** The full URL of the icon from Discords CDN. Undefined when no icon is set. */
|
||||
iconURL(size?: ImageSize, format?: ImageFormats): string | undefined;
|
||||
/** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event. */
|
||||
delete(): Promise<any>;
|
||||
/** Leave a guild */
|
||||
leave(): Promise<any>;
|
||||
/** Edit the server. Requires the MANAGE_GUILD permission. */
|
||||
edit(options: GuildEditOptions): Promise<any>;
|
||||
/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */
|
||||
auditLogs(options: GetAuditLogsOptions): Promise<any>;
|
||||
/** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */
|
||||
getBan(memberID: string): Promise<BannedUser>;
|
||||
/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */
|
||||
bans(): Promise<Collection<string, BannedUser>>;
|
||||
/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */
|
||||
ban(memberID: string, options: BanOptions): Promise<any>;
|
||||
/** Remove the ban for a user. Requires BAN_MEMBERS permission */
|
||||
unban(memberID: string): Promise<any>;
|
||||
/** Get all the invites for this guild. Requires MANAGE_GUILD permission */
|
||||
invites(): Promise<any>;
|
||||
}
|
||||
|
||||
interface CleanVoiceState extends VoiceState {
|
||||
/** The guild id where this voice state is from */
|
||||
guildID: string;
|
||||
/** The channel id where this voice state is from */
|
||||
channelID: string;
|
||||
/** The user id */
|
||||
userID: string;
|
||||
/** The unique random session id for this voice session */
|
||||
sessionID: string;
|
||||
/** Whether the user has deafened themself */
|
||||
selfDeaf: boolean;
|
||||
/** Whether the user has muted themself */
|
||||
selfMute: boolean;
|
||||
/** Whether the user is streaming on go live */
|
||||
selfStream: boolean;
|
||||
}
|
||||
188
src/api/structures/member.ts
Normal file
188
src/api/structures/member.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
BanOptions,
|
||||
EditMemberOptions,
|
||||
GuildMember,
|
||||
ImageFormats,
|
||||
ImageSize,
|
||||
MemberCreatePayload,
|
||||
MessageContent,
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { createNewProp } from "../../util/utils.ts";
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { ban } from "../handlers/guild.ts";
|
||||
import {
|
||||
addRole,
|
||||
editMember,
|
||||
kick,
|
||||
rawAvatarURL,
|
||||
removeRole,
|
||||
sendDirectMessage,
|
||||
} from "../handlers/member.ts";
|
||||
import { Guild } from "./guild.ts";
|
||||
|
||||
const baseMember: Partial<Member> = {
|
||||
get avatarURL() {
|
||||
return rawAvatarURL(this.id!, this.discriminator!, this.avatar!);
|
||||
},
|
||||
get mention() {
|
||||
return `<@!${this.id!}>`;
|
||||
},
|
||||
get tag() {
|
||||
return `${this.username!}#${this.discriminator!}`;
|
||||
},
|
||||
|
||||
// METHODS
|
||||
makeAvatarURL(options) {
|
||||
return rawAvatarURL(
|
||||
this.id!,
|
||||
this.discriminator!,
|
||||
this.avatar!,
|
||||
options.size,
|
||||
options.format,
|
||||
);
|
||||
},
|
||||
guild(guildID) {
|
||||
return cache.guilds.get(guildID);
|
||||
},
|
||||
name(guildID) {
|
||||
return this.guildMember!(guildID)?.nick || this.username!;
|
||||
},
|
||||
guildMember(guildID) {
|
||||
return this.guilds?.get(guildID);
|
||||
},
|
||||
sendDM(content) {
|
||||
return sendDirectMessage(this.id!, content);
|
||||
},
|
||||
kick(guildID, reason) {
|
||||
return kick(guildID, this.id!, reason);
|
||||
},
|
||||
edit(guildID, options) {
|
||||
return editMember(guildID, this.id!, options);
|
||||
},
|
||||
ban(guildID, options) {
|
||||
return ban(guildID, this.id!, options);
|
||||
},
|
||||
addRole(guildID, roleID, reason) {
|
||||
return addRole(guildID, this.id!, roleID, reason);
|
||||
},
|
||||
removeRole(guildID, roleID, reason) {
|
||||
return removeRole(guildID, this.id!, roleID, reason);
|
||||
},
|
||||
};
|
||||
|
||||
export async function createMember(data: MemberCreatePayload, guildID: string) {
|
||||
const {
|
||||
joined_at: joinedAt,
|
||||
premium_since: premiumSince,
|
||||
user: userData,
|
||||
roles,
|
||||
deaf,
|
||||
mute,
|
||||
nick,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const { mfa_enabled: mfaEnabled, premium_type: premiumType, ...user } =
|
||||
data.user || {};
|
||||
|
||||
const restProps: Record<string, ReturnType<typeof createNewProp>> = {};
|
||||
for (const key of Object.keys(rest)) {
|
||||
restProps[key] = createNewProp((rest as any)[key]);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(user)) {
|
||||
// @ts-ignore
|
||||
restProps[key] = createNewProp(user[key]);
|
||||
}
|
||||
|
||||
const member = Object.create(baseMember, {
|
||||
...restProps,
|
||||
mfaEnabled: createNewProp(mfaEnabled),
|
||||
premiumType: createNewProp(premiumType),
|
||||
/** The guild related data mapped by guild id */
|
||||
guilds: createNewProp(new Collection<string, GuildMember>()),
|
||||
});
|
||||
|
||||
const cached = await cacheHandlers.get("members", user.id);
|
||||
if (cached) {
|
||||
for (const [id, guild] of cached.guilds.entries()) {
|
||||
member.guilds.set(id, guild);
|
||||
}
|
||||
}
|
||||
|
||||
// User was never cached before
|
||||
member.guilds.set(guildID, {
|
||||
nick: nick,
|
||||
roles: roles,
|
||||
joinedAt: Date.parse(joinedAt),
|
||||
premiumSince: premiumSince ? Date.parse(premiumSince) : undefined,
|
||||
deaf: deaf,
|
||||
mute: mute,
|
||||
});
|
||||
|
||||
await cacheHandlers.set("members", member.id, member);
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
/** The user's id */
|
||||
id: string;
|
||||
/** the user's username, not unique across the platform */
|
||||
username: string;
|
||||
/** The user's 4 digit discord tag */
|
||||
discriminator: string;
|
||||
/** The user's avatar hash */
|
||||
avatar: string | null;
|
||||
/** Whether the user is a bot */
|
||||
bot?: boolean;
|
||||
/** Whether the user is an official discord system user (part of the urgent message system.) */
|
||||
system?: boolean;
|
||||
/** the user's chosen language option */
|
||||
locale?: string;
|
||||
/** Whether the email on this account has been verified */
|
||||
verified?: boolean;
|
||||
/** The user's email */
|
||||
email?: string;
|
||||
/** The flags on a user's account. */
|
||||
flags?: number;
|
||||
/** Whether or not this user has 2FA enabled. */
|
||||
mfaEnabled?: boolean;
|
||||
/** The premium type for this user */
|
||||
premiumType?: number;
|
||||
/** The guild related data mapped by guild id */
|
||||
guilds: Collection<string, GuildMember>;
|
||||
|
||||
// GETTERS
|
||||
/** The avatar url using the default format and size. */
|
||||
avatarURL: string;
|
||||
/** The mention string for this member */
|
||||
mention: string;
|
||||
/** The username#discriminator tag for this member */
|
||||
tag: string;
|
||||
|
||||
// METHODS
|
||||
|
||||
/** Returns the avatar url for this member and can be dynamically modified with a size or format */
|
||||
makeAvatarURL(options: { size?: ImageSize; format?: ImageFormats }): string;
|
||||
/** Returns the guild for this guildID */
|
||||
guild(guildID: string): Guild | undefined;
|
||||
/** Get the nickname or the username if no nickname */
|
||||
name(guildID: string): string;
|
||||
/** Get the nickname */
|
||||
guildMember(guildID: string): GuildMember | undefined;
|
||||
/** Send a direct message to the user is possible */
|
||||
sendDM(content: string | MessageContent): Promise<any>;
|
||||
/** Kick the member from a guild */
|
||||
kick(guildID: string, reason?: string): Promise<any>;
|
||||
/** Edit the member in a guild */
|
||||
edit(guildID: string, options: EditMemberOptions): Promise<any>;
|
||||
/** Ban a member in a guild */
|
||||
ban(guildID: string, options: BanOptions): Promise<any>;
|
||||
/** Add a role to the member */
|
||||
addRole(guildID: string, roleID: string, reason?: string): Promise<any>;
|
||||
/** Remove a role from the member */
|
||||
removeRole(guildID: string, roleID: string, reason?: string): Promise<any>;
|
||||
}
|
||||
264
src/api/structures/message.ts
Normal file
264
src/api/structures/message.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
Activity,
|
||||
Application,
|
||||
Attachment,
|
||||
Embed,
|
||||
GuildMember,
|
||||
MessageContent,
|
||||
MessageCreateOptions,
|
||||
MessageSticker,
|
||||
Reaction,
|
||||
Reference,
|
||||
UserPayload,
|
||||
} from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { createNewProp } from "../../util/utils.ts";
|
||||
import { sendMessage } from "../handlers/channel.ts";
|
||||
import {
|
||||
addReaction,
|
||||
addReactions,
|
||||
deleteMessageByID,
|
||||
editMessage,
|
||||
pin,
|
||||
removeAllReactions,
|
||||
removeReaction,
|
||||
removeReactionEmoji,
|
||||
} from "../handlers/message.ts";
|
||||
import { Channel } from "./channel.ts";
|
||||
import { Guild } from "./guild.ts";
|
||||
import { Member } from "./member.ts";
|
||||
import { Role } from "./role.ts";
|
||||
|
||||
const baseMessage: Partial<Message> = {
|
||||
get channel() {
|
||||
return cache.channels.get(this.channelID!);
|
||||
},
|
||||
get guild() {
|
||||
if (!this.guildID) return;
|
||||
return cache.guilds.get(this.guildID);
|
||||
},
|
||||
get member() {
|
||||
if (!this.author?.id) return;
|
||||
return cache.members.get(this.author?.id);
|
||||
},
|
||||
get guildMember() {
|
||||
if (!this.guildID) return;
|
||||
return this.member?.guilds.get(this.guildID);
|
||||
},
|
||||
get link() {
|
||||
return `https://discord.com/channels/${this.guildID ||
|
||||
"@me"}/${this.channelID}/${this.id}`;
|
||||
},
|
||||
get mentionedRoles() {
|
||||
// TODO: add getters for Guild structure, that will fix this error
|
||||
return this.mentionRoleIDs?.map((id) => this.guild?.roles.get(id)) || [];
|
||||
},
|
||||
get mentionedChannels() {
|
||||
return this.mentionChannelIDs?.map((id) => cache.channels.get(id)) || [];
|
||||
},
|
||||
get mentionedMembers() {
|
||||
return this.mentions?.map((id) => cache.members.get(id)) || [];
|
||||
},
|
||||
|
||||
// METHODS
|
||||
delete(reason, delayMilliseconds) {
|
||||
return deleteMessageByID(
|
||||
this.channelID!,
|
||||
this.id!,
|
||||
reason,
|
||||
delayMilliseconds,
|
||||
);
|
||||
},
|
||||
edit(content) {
|
||||
return editMessage(this as Message, content);
|
||||
},
|
||||
pin() {
|
||||
return pin(this.channelID!, this.id!);
|
||||
},
|
||||
addReaction(reaction) {
|
||||
return addReaction(this.channelID!, this.id!, reaction);
|
||||
},
|
||||
addReactions(reactions, ordered) {
|
||||
return addReactions(this.channelID!, this.id!, reactions, ordered);
|
||||
},
|
||||
reply(content) {
|
||||
const contentWithMention = typeof content === "string"
|
||||
? { content, mentions: { repliedUser: true }, replyMessageID: this.id }
|
||||
: {
|
||||
...content,
|
||||
mentions: { ...(content.mentions || {}), repliedUser: true },
|
||||
replyMessageID: this.id,
|
||||
};
|
||||
|
||||
return sendMessage(this.channelID!, contentWithMention);
|
||||
},
|
||||
send(content) {
|
||||
return sendMessage(this.channelID!, content);
|
||||
},
|
||||
alert(content, timeout = 10, reason = "") {
|
||||
return sendMessage(this.channelID!, content).then((response) => {
|
||||
response.delete(reason, timeout * 1000).catch(console.error);
|
||||
});
|
||||
},
|
||||
alertReply(content, timeout = 10, reason = "") {
|
||||
return this.reply!(content).then((response) =>
|
||||
response.delete(reason, timeout * 1000).catch(console.error)
|
||||
);
|
||||
},
|
||||
removeAllReactions() {
|
||||
return removeAllReactions(this.channelID!, this.id!);
|
||||
},
|
||||
removeReactionEmoji(reaction) {
|
||||
return removeReactionEmoji(this.channelID!, this.id!, reaction);
|
||||
},
|
||||
removeReaction(reaction) {
|
||||
return removeReaction(this.channelID!, this.id!, reaction);
|
||||
},
|
||||
};
|
||||
|
||||
export async function createMessage(data: MessageCreateOptions) {
|
||||
const {
|
||||
guild_id: guildID,
|
||||
channel_id: channelID,
|
||||
mentions_everyone: mentionsEveryone,
|
||||
mention_channels: mentionChannelIDs,
|
||||
mention_roles: mentionRoleIDs,
|
||||
webhook_id: webhookID,
|
||||
message_reference: messageReference,
|
||||
edited_timestamp: editedTimestamp,
|
||||
referenced_message: referencedMessageID,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const restProps: Record<string, ReturnType<typeof createNewProp>> = {};
|
||||
for (const key of Object.keys(rest)) {
|
||||
restProps[key] = createNewProp((rest as any)[key]);
|
||||
}
|
||||
|
||||
const message = Object.create(baseMessage, {
|
||||
...restProps,
|
||||
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
|
||||
referencedMessageID: createNewProp(referencedMessageID),
|
||||
channelID: createNewProp(channelID),
|
||||
guildID: createNewProp(guildID || ""),
|
||||
mentions: createNewProp(data.mentions.map((m) => m.id)),
|
||||
mentionsEveryone: createNewProp(mentionsEveryone),
|
||||
mentionRoleIDs: createNewProp(mentionRoleIDs),
|
||||
mentionChannelIDs: createNewProp(mentionChannelIDs?.map((m) => m.id) || []),
|
||||
webhookID: createNewProp(webhookID),
|
||||
messageReference: createNewProp(messageReference),
|
||||
timestamp: createNewProp(Date.parse(data.timestamp)),
|
||||
editedTimestamp: createNewProp(
|
||||
editedTimestamp ? Date.parse(editedTimestamp) : undefined,
|
||||
),
|
||||
});
|
||||
|
||||
return message as Message;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
/** The id of the message */
|
||||
id: string;
|
||||
/** The id of the channel the message was sent in */
|
||||
channelID: string;
|
||||
/** The id of the guild the message was sent in */
|
||||
guildID: string;
|
||||
/** The author of this message (not guaranteed to be a valid user such as a webhook.) */
|
||||
author: UserPayload;
|
||||
/** The contents of the message */
|
||||
content: string;
|
||||
/** When this message was sent */
|
||||
timestamp: number;
|
||||
/** When this message was edited (if it was not edited, null) */
|
||||
editedTimestamp?: number;
|
||||
/** Whether this was a TextToSpeech message. */
|
||||
tts: boolean;
|
||||
/** Whether this message mentions everyone */
|
||||
mentionsEveryone: boolean;
|
||||
/** Users specifically mentioned in the message. */
|
||||
mentions: string[];
|
||||
/** Roles specifically mentioned in this message */
|
||||
mentionRoleIDs: string[];
|
||||
/** Channels specifically mentioned in this message */
|
||||
mentionChannelIDs: string[];
|
||||
/** Any attached files */
|
||||
attachments: Attachment[];
|
||||
/** Any embedded content */
|
||||
embeds: Embed[];
|
||||
/** Reactions to the message */
|
||||
reactions?: Reaction[];
|
||||
/** Used for validating a message was sent */
|
||||
nonce?: number | string;
|
||||
/** Whether this message is pinned */
|
||||
pinned: boolean;
|
||||
/** If the message is generated by a webhook, this is the webhooks id */
|
||||
webhook_id?: string;
|
||||
/** The type of message */
|
||||
type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
/** The activities sent with Rich Presence-related chat embeds. */
|
||||
activity?: Activity;
|
||||
/** Applications that sent with Rich Presence related chat embeds. */
|
||||
applications?: Application;
|
||||
/** The reference data sent with crossposted messages */
|
||||
messageReference?: Reference;
|
||||
/** The message flags combined like permission bits describe extra features of the message */
|
||||
flags?: 1 | 2 | 4 | 8 | 16;
|
||||
/** the stickers sent with the message (bots currently can only receive messages with stickers, not send) */
|
||||
stickers?: MessageSticker[];
|
||||
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
|
||||
referencedMessageID?: MessageCreateOptions | null;
|
||||
|
||||
// GETTERS
|
||||
|
||||
/** The channel where this message was sent. Can be undefined if uncached. */
|
||||
channel?: Channel;
|
||||
/** The guild of this message. Can be undefined if not in cache or in DM */
|
||||
guild?: Guild;
|
||||
/** The member for the user who sent the message. Can be undefined if not in cache or in dm. */
|
||||
member?: Member;
|
||||
/** The guild member details for this guild and member. Can be undefined if not in cache or in dm. */
|
||||
guildMember?: GuildMember;
|
||||
/** The url link to this message */
|
||||
link: string;
|
||||
/** The role objects for all the roles that were mentioned in this message */
|
||||
mentionedRoles: (Role | undefined)[];
|
||||
/** The channel objects for all the channels that were mentioned in this message. */
|
||||
mentionedChannels: (Channel | undefined)[];
|
||||
/** The member objects for all the members that were mentioned in this message. */
|
||||
mentionedMembers: (Member | undefined)[];
|
||||
|
||||
// METHODS
|
||||
|
||||
/** Delete the message */
|
||||
delete(reason?: string, delayMilliseconds?: number): Promise<unknown>;
|
||||
/** Edit the message */
|
||||
edit(content: string | MessageContent): Promise<Message>;
|
||||
/** Pins the message in the channel */
|
||||
pin(): Promise<void>;
|
||||
/** Add a reaction to the message */
|
||||
addReaction(reaction: string): Promise<unknown>;
|
||||
/** Add multiple reactions to the message without or without order. */
|
||||
addReactions(reactions: string[], ordered?: boolean): Promise<void>;
|
||||
/** Send a inline reply to this message */
|
||||
reply(content: string | MessageContent): Promise<Message>;
|
||||
/** Send a message to this channel where this message is */
|
||||
send(content: string | MessageContent): Promise<Message>;
|
||||
/** Send a message to this channel and then delete it after a bit. By default it will delete after 10 seconds with no reason provided. */
|
||||
alert(
|
||||
content: string | MessageContent,
|
||||
timeout?: number,
|
||||
reason?: string,
|
||||
): Promise<void>;
|
||||
/** Send a inline reply to this message but then delete it after a bit. By default it will delete after 10 seconds with no reason provided. */
|
||||
alertReply(
|
||||
content: string | MessageContent,
|
||||
timeout?: number,
|
||||
reason?: string,
|
||||
): Promise<unknown>;
|
||||
/** Remove all reactions */
|
||||
removeAllReactions(): Promise<unknown>;
|
||||
/** Remove all reactions */
|
||||
removeReactionEmoji(reaction: string): Promise<any>;
|
||||
/** Remove all reactions */
|
||||
removeReaction(reaction: string): Promise<any>;
|
||||
}
|
||||
@@ -17,9 +17,10 @@ export let structures = {
|
||||
|
||||
export type Structures = typeof structures;
|
||||
|
||||
/** This function is used to update/reload/customize the internal structure of Discordeno.
|
||||
/** This function is used to update/reload/customize the internal structures of Discordeno.
|
||||
*
|
||||
* ⚠️ **ADVANCED USE ONLY: If you customize this in a wrong way, you could potentially create many new errors/bugs. Please take caution when using this.
|
||||
* ⚠️ **ADVANCED USE ONLY: If you customize this incorrectly, you could potentially create many new errors/bugs.
|
||||
* Please take caution when using this.**
|
||||
*/
|
||||
export function updateStructures(newStructures: Structures) {
|
||||
structures = {
|
||||
@@ -27,3 +28,10 @@ export function updateStructures(newStructures: Structures) {
|
||||
...newStructures,
|
||||
};
|
||||
}
|
||||
|
||||
export type { Channel } from "./channel.ts";
|
||||
export type { Guild } from "./guild.ts";
|
||||
export type { Member } from "./member.ts";
|
||||
export type { Message } from "./message.ts";
|
||||
export type { Role } from "./role.ts";
|
||||
export type { Template } from "./template.ts";
|
||||
130
src/api/structures/role.ts
Normal file
130
src/api/structures/role.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { CreateRoleOptions, RoleData } from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { createNewProp } from "../../util/utils.ts";
|
||||
import { deleteRole, editRole } from "../handlers/guild.ts";
|
||||
import { Guild } from "./guild.ts";
|
||||
import { Member } from "./member.ts";
|
||||
|
||||
const baseRole: any = {
|
||||
get guild() {
|
||||
return cache.guilds.find((g) => g.roles.has(this.id));
|
||||
},
|
||||
get hexColor() {
|
||||
return this.color!.toString(16);
|
||||
},
|
||||
get members() {
|
||||
return cache.members.filter((m) =>
|
||||
m.guilds.some((g) => g.roles.includes(this.id))
|
||||
);
|
||||
},
|
||||
get mention() {
|
||||
return `<@&${this.id}>`;
|
||||
},
|
||||
|
||||
// METHODS
|
||||
delete(guildID?: string) {
|
||||
// If not guild id was provided try and find one
|
||||
if (!guildID) guildID = guildID || this.guild?.id;
|
||||
// If a guild id is still not available error out
|
||||
if (!guildID) {
|
||||
throw new Error(
|
||||
"role.delete() did not find a valid guild in cache. Please provide the guildID like role.delete(guildID)",
|
||||
);
|
||||
}
|
||||
|
||||
return deleteRole(guildID, this.id!).catch(console.error);
|
||||
},
|
||||
edit(options: CreateRoleOptions, guildID?: string) {
|
||||
// If not guild id was provided try and find one
|
||||
if (!guildID) guildID = guildID || this.guild?.id;
|
||||
// If a guild id is still not available error out
|
||||
if (!guildID) {
|
||||
throw new Error(
|
||||
"role.edit() did not find a valid guild in cache. Please provide the guildID like role.edit({}, guildID)",
|
||||
);
|
||||
}
|
||||
|
||||
return editRole(guildID, this.id!, options);
|
||||
},
|
||||
higherThanRoleID(roleID: string, position?: number) {
|
||||
// If no position try and find one from cache
|
||||
if (!position) position = this.guild?.roles.get(roleID)?.position;
|
||||
// If still none error out.
|
||||
if (!position) {
|
||||
throw new Error(
|
||||
"role.higherThanRoleID() did not have a position provided and the role or guild was not found in cache. Please provide a position like role.higherThanRoleID(roleID, position)",
|
||||
);
|
||||
}
|
||||
|
||||
// Rare edge case handling
|
||||
if (this.position === position) {
|
||||
return this.id! < roleID;
|
||||
}
|
||||
|
||||
return this.position! > position;
|
||||
},
|
||||
};
|
||||
|
||||
export async function createRole(data: RoleData) {
|
||||
const { tags, ...rest } = data;
|
||||
|
||||
const restProps: Record<string, ReturnType<typeof createNewProp>> = {};
|
||||
for (const key of Object.keys(rest)) {
|
||||
restProps[key] = createNewProp((rest as any)[key]);
|
||||
}
|
||||
|
||||
const role = Object.create(baseRole, {
|
||||
...restProps,
|
||||
botID: createNewProp(tags?.bot_id),
|
||||
isNitroBoostRole: createNewProp("premium_subscriber" in (tags ?? {})),
|
||||
integrationID: createNewProp(tags?.integration_id),
|
||||
});
|
||||
|
||||
return role as Role;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
/** role id */
|
||||
id: string;
|
||||
/** role name */
|
||||
name: string;
|
||||
/** integer representation of hexadecimal color code */
|
||||
color: number;
|
||||
/** if this role is pinned in the user listing */
|
||||
hoist: boolean;
|
||||
/** position of this role */
|
||||
position: number;
|
||||
/** permission bit set */
|
||||
permissions: string;
|
||||
/** whether this role is managed by an integration */
|
||||
managed: boolean;
|
||||
/** whether this role is mentionable */
|
||||
mentionable: boolean;
|
||||
/** The bot id that is associated with this role. */
|
||||
botID?: string;
|
||||
/** If this role is the nitro boost role. */
|
||||
isNitroBoostRole: boolean;
|
||||
/** The integration id that is associated with this role */
|
||||
integrationID: string;
|
||||
|
||||
// GETTERS
|
||||
|
||||
/** The guild where this role is. If undefined, the guild is not cached */
|
||||
guild?: Guild;
|
||||
/** The hex color for this role. */
|
||||
hexColor: string;
|
||||
/** The cached members that have this role */
|
||||
members: Collection<string, Member>;
|
||||
/** The @ mention of the role in a string. */
|
||||
mention: string;
|
||||
|
||||
// METHODS
|
||||
|
||||
/** Delete the role */
|
||||
delete(guildID?: string): Promise<unknown>;
|
||||
/** Edits the role */
|
||||
edit(options: CreateRoleOptions): Promise<unknown>;
|
||||
/** Checks if this role is higher than another role. */
|
||||
higherThanRoleID(roleID: string, position?: number): boolean;
|
||||
}
|
||||
7
src/api/structures/structures.ts
Normal file
7
src/api/structures/structures.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./channel.ts";
|
||||
export * from "./guild.ts";
|
||||
export * from "./member.ts";
|
||||
export * from "./message.ts";
|
||||
export * from "./mod.ts";
|
||||
export * from "./role.ts";
|
||||
export * from "./template.ts";
|
||||
72
src/api/structures/template.ts
Normal file
72
src/api/structures/template.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { GuildTemplate, UserPayload } from "../../types/mod.ts";
|
||||
import { cache } from "../../util/cache.ts";
|
||||
import { createNewProp } from "../../util/utils.ts";
|
||||
import { Guild } from "./guild.ts";
|
||||
|
||||
const baseTemplate: any = {
|
||||
get sourceGuild() {
|
||||
return cache.guilds.get(this.sourceGuildID);
|
||||
},
|
||||
};
|
||||
|
||||
export function createTemplate(
|
||||
data: GuildTemplate,
|
||||
) {
|
||||
const {
|
||||
usage_count: usageCount,
|
||||
creator_id: creatorID,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
source_guild_id: sourceGuildID,
|
||||
serialized_source_guild: serializedSourceGuild,
|
||||
is_dirty: isDirty,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const restProps: Record<string, Partial<PropertyDescriptor>> = {};
|
||||
for (const key of Object.keys(rest)) {
|
||||
restProps[key] = createNewProp((rest as any)[key]);
|
||||
}
|
||||
|
||||
const template = Object.create(baseTemplate, {
|
||||
...restProps,
|
||||
usageCount: createNewProp(sourceGuildID),
|
||||
creatorID: createNewProp(creatorID),
|
||||
createdAt: createNewProp(createdAt),
|
||||
updatedAt: createNewProp(updatedAt),
|
||||
sourceGuildID: createNewProp(sourceGuildID),
|
||||
serializedSourceGuild: createNewProp(serializedSourceGuild),
|
||||
isDirty: createNewProp(isDirty),
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
/** the template code (unique ID) */
|
||||
code: string;
|
||||
/** template name */
|
||||
name: string;
|
||||
/** the description for the template */
|
||||
description: string | null;
|
||||
/** number of times this template has been used */
|
||||
usageCount: number;
|
||||
/** the ID of the user who created the template */
|
||||
createdID: string;
|
||||
/** the user who created the template */
|
||||
creator: UserPayload;
|
||||
/** when this template was created */
|
||||
createdAt: string;
|
||||
/** when this template was last synced to the source guild */
|
||||
updatedAt: string;
|
||||
/** the ID of the guild this template is based on */
|
||||
sourceGuildID: string;
|
||||
/** the guild snapshot this template contains */
|
||||
serializedSourceGuild: Partial<Guild>;
|
||||
/** whether the template has unsynced changes */
|
||||
isDirty: boolean | null;
|
||||
|
||||
// GETTERS
|
||||
|
||||
sourceGuild: Guild | undefined;
|
||||
}
|
||||
130
src/bot.ts
Normal file
130
src/bot.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { RequestManager } from "./rest/mod.ts";
|
||||
import {
|
||||
BotConfig,
|
||||
DiscordBotGatewayData,
|
||||
EventHandlers,
|
||||
} from "./types/mod.ts";
|
||||
import { baseEndpoints, endpoints, GATEWAY_VERSION } from "./util/constants.ts";
|
||||
import { spawnShards } from "./ws/shard_manager.ts";
|
||||
|
||||
export let authorization = "";
|
||||
export let botID = "";
|
||||
|
||||
export let eventHandlers: EventHandlers = {};
|
||||
|
||||
export let botGatewayData: DiscordBotGatewayData;
|
||||
export let proxyWSURL = `wss://gateway.discord.gg`;
|
||||
|
||||
export const identifyPayload: IdentifyPayload = {
|
||||
token: "",
|
||||
compress: true,
|
||||
properties: {
|
||||
$os: "linux",
|
||||
$browser: "Discordeno",
|
||||
$device: "Discordeno",
|
||||
},
|
||||
intents: 0,
|
||||
shard: [0, 0],
|
||||
};
|
||||
|
||||
export interface IdentifyPayload {
|
||||
token: string;
|
||||
compress: boolean;
|
||||
properties: {
|
||||
$os: string;
|
||||
$browser: string;
|
||||
$device: string;
|
||||
};
|
||||
intents: number;
|
||||
shard: [number, number];
|
||||
}
|
||||
|
||||
export async function startBot(config: BotConfig) {
|
||||
if (config.eventHandlers) eventHandlers = config.eventHandlers;
|
||||
authorization = `Bot ${config.token}`;
|
||||
|
||||
// Initial API connection to get info about bots connection
|
||||
botGatewayData = await RequestManager.get(
|
||||
endpoints.GATEWAY_BOT,
|
||||
) as DiscordBotGatewayData;
|
||||
|
||||
// Explicitly append gateway version and encoding
|
||||
botGatewayData.url += `?v=${GATEWAY_VERSION}&encoding=json`;
|
||||
|
||||
proxyWSURL = botGatewayData.url;
|
||||
identifyPayload.token = config.token;
|
||||
identifyPayload.intents = config.intents.reduce(
|
||||
(bits, next) => (bits |= next),
|
||||
0,
|
||||
);
|
||||
identifyPayload.shard = [0, botGatewayData.shards];
|
||||
|
||||
spawnShards(botGatewayData, identifyPayload, 0, botGatewayData.shards);
|
||||
}
|
||||
|
||||
/** Allows you to dynamically update the event handlers by passing in new eventHandlers */
|
||||
export function updateEventHandlers(newEventHandlers: EventHandlers) {
|
||||
eventHandlers = {
|
||||
...eventHandlers,
|
||||
...newEventHandlers,
|
||||
};
|
||||
}
|
||||
|
||||
/** INTERNAL LIB function used to set the bot ID once the READY event is sent by Discord. */
|
||||
export function setBotID(id: string) {
|
||||
if (botID !== id) botID = id;
|
||||
}
|
||||
|
||||
// BIG BRAIN BOT STUFF ONLY BELOW THIS
|
||||
|
||||
/**
|
||||
* This function should be used only by bot developers whose bots are in over 25,000 servers.
|
||||
* Please be aware if you are a beginner developer using this, things will not work as per the guides. This is for advanced developers only!
|
||||
*
|
||||
* Advanced Devs: This function will allow you to have an insane amount of customization potential as when you get to large bots you need to be able to optimize every tiny detail to make you bot work the way you need.
|
||||
*/
|
||||
export async function startBigBrainBot(data: BigBrainBotConfig) {
|
||||
authorization = `Bot ${data.token}`;
|
||||
identifyPayload.token = `Bot ${data.token}`;
|
||||
|
||||
if (data.restURL) baseEndpoints.BASE_URL = data.restURL;
|
||||
if (data.cdnURL) baseEndpoints.CDN_URL = data.cdnURL;
|
||||
if (data.wsURL) proxyWSURL = data.wsURL;
|
||||
if (data.eventHandlers) eventHandlers = data.eventHandlers;
|
||||
if (data.compress) {
|
||||
identifyPayload.compress = data.compress;
|
||||
}
|
||||
|
||||
identifyPayload.intents = data.intents.reduce(
|
||||
(bits, next) => (bits |= next),
|
||||
0,
|
||||
);
|
||||
|
||||
// Initial API connection to get info about bots connection
|
||||
botGatewayData = await RequestManager.get(
|
||||
endpoints.GATEWAY_BOT,
|
||||
) as DiscordBotGatewayData;
|
||||
|
||||
if (!data.wsURL) proxyWSURL = botGatewayData.url;
|
||||
spawnShards(
|
||||
botGatewayData,
|
||||
identifyPayload,
|
||||
data.firstShardID,
|
||||
data.lastShardID || botGatewayData.shards >= 25
|
||||
? (data.firstShardID + 25)
|
||||
: botGatewayData.shards,
|
||||
);
|
||||
}
|
||||
|
||||
export interface BigBrainBotConfig extends BotConfig {
|
||||
/** The first shard to start at for this worker. Use this to control which shards to run in each worker. */
|
||||
firstShardID: number;
|
||||
/** The last shard to start for this worker. By default it will be 25 + the firstShardID. */
|
||||
lastShardID?: number;
|
||||
/** This can be used to forward the ws handling to a proxy. */
|
||||
wsURL?: string;
|
||||
/** This can be used to forward the REST handling to a proxy. */
|
||||
restURL?: string;
|
||||
/** This can be used to forward the CDN handling to a proxy. */
|
||||
cdnURL?: string;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { RequestManager } from "../module/requestManager.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import { Errors } from "../types/errors.ts";
|
||||
import { MessageCreateOptions } from "../types/message.ts";
|
||||
import { Permissions } from "../types/permission.ts";
|
||||
import {
|
||||
ExecuteWebhookOptions,
|
||||
WebhookCreateOptions,
|
||||
WebhookPayload,
|
||||
} from "../types/webhook.ts";
|
||||
import { endpoints } from "../utils/constants.ts";
|
||||
import { botHasChannelPermissions } from "../utils/permissions.ts";
|
||||
import { urlToBase64 } from "../utils/utils.ts";
|
||||
|
||||
/** Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations:
|
||||
*
|
||||
* Webhook names cannot be: 'clyde'
|
||||
*/
|
||||
export async function createWebhook(
|
||||
channelID: string,
|
||||
options: WebhookCreateOptions,
|
||||
) {
|
||||
const hasManageWebhooksPerm = await botHasChannelPermissions(
|
||||
channelID,
|
||||
[Permissions.MANAGE_WEBHOOKS],
|
||||
);
|
||||
if (
|
||||
!hasManageWebhooksPerm
|
||||
) {
|
||||
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
|
||||
}
|
||||
|
||||
if (
|
||||
// Specific usernames that discord does not allow
|
||||
options.name === "clyde" ||
|
||||
// Character limit checks. [...] checks are because of js unicode length handling
|
||||
[...options.name].length < 2 || [...options.name].length > 32
|
||||
) {
|
||||
throw new Error(Errors.INVALID_WEBHOOK_NAME);
|
||||
}
|
||||
|
||||
return RequestManager.post(
|
||||
endpoints.CHANNEL_WEBHOOKS(channelID),
|
||||
{
|
||||
...options,
|
||||
avatar: options.avatar ? await urlToBase64(options.avatar) : undefined,
|
||||
},
|
||||
) as Promise<WebhookPayload>;
|
||||
}
|
||||
|
||||
export async function executeWebhook(
|
||||
webhookID: string,
|
||||
webhookToken: string,
|
||||
options: ExecuteWebhookOptions,
|
||||
) {
|
||||
if (!options.content && !options.file && !options.embeds) {
|
||||
throw new Error(Errors.INVALID_WEBHOOK_OPTIONS);
|
||||
}
|
||||
|
||||
if (options.embeds && options.embeds.length > 10) {
|
||||
options.embeds.splice(10);
|
||||
}
|
||||
|
||||
if (options.mentions) {
|
||||
if (options.mentions.users?.length) {
|
||||
if (options.mentions.parse.includes("users")) {
|
||||
options.mentions.parse = options.mentions.parse.filter((p) =>
|
||||
p !== "users"
|
||||
);
|
||||
}
|
||||
|
||||
if (options.mentions.users.length > 100) {
|
||||
options.mentions.users = options.mentions.users.slice(0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.mentions.roles?.length) {
|
||||
if (options.mentions.parse.includes("roles")) {
|
||||
options.mentions.parse = options.mentions.parse.filter((p) =>
|
||||
p !== "roles"
|
||||
);
|
||||
}
|
||||
|
||||
if (options.mentions.roles.length > 100) {
|
||||
options.mentions.roles = options.mentions.roles.slice(0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await RequestManager.post(
|
||||
`${endpoints.WEBHOOK(webhookID, webhookToken)}${
|
||||
options.wait ? "?wait=true" : ""
|
||||
}`,
|
||||
{
|
||||
...options,
|
||||
allowed_mentions: options.mentions,
|
||||
avatar_url: options.avatar_url,
|
||||
},
|
||||
);
|
||||
if (!options.wait) return;
|
||||
|
||||
return structures.createMessage(result as MessageCreateOptions);
|
||||
}
|
||||
|
||||
export function getWebhook(webhookID: string) {
|
||||
return RequestManager.get(endpoints.WEBHOOK_ID(webhookID));
|
||||
}
|
||||
8
src/interactions/README.md
Normal file
8
src/interactions/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Discordeno Interactions
|
||||
|
||||
`interactions` is a standalone submodule that supports the webhooks through HTTP server to add support for Discord's interactions feature.
|
||||
This is a barebones interface that will create and handle requests from Discord API.
|
||||
|
||||
- Complete and extremely fast security and verification checks
|
||||
- First-class TypeScript Support
|
||||
- Minimalistic
|
||||
2
src/interactions/deps.ts
Normal file
2
src/interactions/deps.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { serve } from "https://deno.land/std@0.81.0/http/server.ts";
|
||||
export { verify } from "https://unpkg.com/@evan/wasm@0.0.25/target/ed25519/deno.js";
|
||||
2
src/interactions/mod.ts
Normal file
2
src/interactions/mod.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./server.ts";
|
||||
export * from "./types/mod.ts";
|
||||
133
src/interactions/server.ts
Normal file
133
src/interactions/server.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { serve, verify } from "./deps.ts";
|
||||
import {
|
||||
Interaction,
|
||||
InteractionResponse,
|
||||
InteractionResponseType,
|
||||
InteractionType,
|
||||
} from "./types/mod.ts";
|
||||
|
||||
/** This variable is a holder for the public key and other configuration */
|
||||
const serverOptions = {
|
||||
publicKey: "",
|
||||
port: 80,
|
||||
};
|
||||
|
||||
/** Theses are the controllers that you can plug into and customize to your needs. */
|
||||
export const controllers = {
|
||||
handlePayload,
|
||||
handleApplicationCommand,
|
||||
};
|
||||
|
||||
export interface StartServerConfig {
|
||||
/** The public key from your discord bot dashboard at discord.dev */
|
||||
publicKey: string;
|
||||
/** The port number you are wanting to listen to, if you are following the guide, you probably want 80 */
|
||||
port: number;
|
||||
/** The function you would like to provide to handle your commands. */
|
||||
handleApplicationCommand?(
|
||||
payload: Interaction,
|
||||
): Promise<{ status?: number; body: InteractionResponse }>;
|
||||
}
|
||||
|
||||
/** Starts the slash command server */
|
||||
export async function startServer(
|
||||
{ port, publicKey, handleApplicationCommand }: StartServerConfig,
|
||||
) {
|
||||
serverOptions.publicKey = publicKey;
|
||||
serverOptions.port = port;
|
||||
if (handleApplicationCommand) {
|
||||
controllers.handleApplicationCommand = handleApplicationCommand;
|
||||
}
|
||||
|
||||
const server = serve({ port: serverOptions.port });
|
||||
|
||||
for await (const req of server) {
|
||||
const buffer = await Deno.readAll(req.body);
|
||||
const signature = req.headers.get("X-Signature-Ed25519");
|
||||
const timestamp = req.headers.get("X-Signature-Timestamp");
|
||||
|
||||
if (!signature || !timestamp) {
|
||||
req.respond({ status: 400, body: "Bad request" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const isVerified = verifySecurity(buffer, signature!, timestamp!);
|
||||
if (!isVerified) {
|
||||
req.respond({ status: 401, body: "Invalid request signature" });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(new TextDecoder().decode(buffer));
|
||||
const response = await controllers.handlePayload(data);
|
||||
req.respond(
|
||||
{ status: response.status || 200, body: JSON.stringify(response.body) },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePayload(payload: Interaction) {
|
||||
switch (payload.type) {
|
||||
case InteractionType.PING:
|
||||
return { status: 200, body: { type: InteractionResponseType.PONG } };
|
||||
default: // APPLICATION_COMMAND
|
||||
return controllers.handleApplicationCommand(payload);
|
||||
}
|
||||
}
|
||||
|
||||
/** The function that handles your commands. This command can be overriden by you and you can receive the payload and handle accordingly and respond back. The status if not provided will default to 200. */
|
||||
async function handleApplicationCommand(
|
||||
payload: Interaction,
|
||||
): Promise<{ status?: number; body: InteractionResponse }> {
|
||||
// Handle the command
|
||||
if (payload.data?.name === "ping") {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: { content: "Pong from Discordeno!" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content:
|
||||
"Whoopsies! Seems the handling for this command is missing. Please contact my developers!",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Internal function to verify security. Discord will send bad and good data and this function is important to verify it. If it is not verified properly, Discord will kill your bot. */
|
||||
function verifySecurity(buffer: Uint8Array, signature: string, time: string) {
|
||||
const sig = new Uint8Array(64);
|
||||
const timestamp = new TextEncoder().encode(time);
|
||||
|
||||
let offset = 0;
|
||||
const message = new Uint8Array(buffer.length + timestamp.length);
|
||||
while (offset < 2 * 64) {
|
||||
sig[offset / 2] = parseInt(signature!.substring(offset, offset += 2), 16);
|
||||
}
|
||||
|
||||
const slash_key = new Uint8Array(32);
|
||||
|
||||
let keyoffset = 0;
|
||||
while (keyoffset < 2 * 32) {
|
||||
slash_key[keyoffset / 2] = parseInt(
|
||||
serverOptions.publicKey.substring(keyoffset, keyoffset += 2),
|
||||
16,
|
||||
);
|
||||
}
|
||||
|
||||
message.set(timestamp);
|
||||
message.set(buffer, timestamp.length);
|
||||
|
||||
return verify(slash_key, sig, message);
|
||||
}
|
||||
95
src/interactions/types/embed.ts
Normal file
95
src/interactions/types/embed.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export interface Embed {
|
||||
/** The title of the embed */
|
||||
title?: string;
|
||||
/** The type of embed (always rich for webhook embeds) */
|
||||
type?: string;
|
||||
/** The description of embeds */
|
||||
description?: string;
|
||||
/** The url of embed */
|
||||
url?: string;
|
||||
/** The timestap of the embed content */
|
||||
timestamp?: string;
|
||||
/** The color code of the embed */
|
||||
color?: number;
|
||||
/** The footer information */
|
||||
footer?: EmbedFooter;
|
||||
/** The image information */
|
||||
image?: EmbedImage;
|
||||
/** The thumbnail information */
|
||||
thumbnail?: EmbedThumbnail;
|
||||
/** The video information */
|
||||
video?: EmbedVideo;
|
||||
/** Provider information */
|
||||
provider?: EmbedProvider;
|
||||
/** Author information */
|
||||
author?: EmbedAuthor;
|
||||
/** Fields information */
|
||||
fields?: EmbedField[];
|
||||
}
|
||||
|
||||
export interface EmbedFooter {
|
||||
/** The text of the footer */
|
||||
text: string;
|
||||
/** The url of the footer icon. Only supports http(s) and attachments */
|
||||
icon_url?: string;
|
||||
/** A proxied url of footer icon */
|
||||
proxy_icon_url?: string;
|
||||
}
|
||||
|
||||
export interface EmbedImage {
|
||||
/** The source url of image (only supports http(s) and attachments) */
|
||||
url?: string;
|
||||
/** A proxied url of the image */
|
||||
proxy_url?: string;
|
||||
/** The height of image */
|
||||
height?: number;
|
||||
/** The width of the image */
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface EmbedThumbnail {
|
||||
/** The source url of image (only supports http(s) and attachments) */
|
||||
url?: string;
|
||||
/** A proxied url of the thumbnail */
|
||||
proxy_url?: string;
|
||||
/** The height of the thumbnail */
|
||||
height?: number;
|
||||
/** The width of the thumbnail */
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface EmbedVideo {
|
||||
/** The source url of video */
|
||||
url?: string;
|
||||
/** The height of the video */
|
||||
height?: number;
|
||||
/** The width of the video */
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface EmbedProvider {
|
||||
/** The name of the provider */
|
||||
name?: string;
|
||||
/** The url of the provider */
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface EmbedAuthor {
|
||||
/** The name of the author */
|
||||
name?: string;
|
||||
/** The url of the author */
|
||||
url?: string;
|
||||
/** The url of the author icon (supports http(s) and attachments) */
|
||||
icon_url?: string;
|
||||
/** A proxied url of author icon */
|
||||
proxy_icon_url?: string;
|
||||
}
|
||||
|
||||
export interface EmbedField {
|
||||
/** The name of the field */
|
||||
name: string;
|
||||
/** The value of the field */
|
||||
value: string;
|
||||
/** Whether or not this field should display inline */
|
||||
inline?: boolean;
|
||||
}
|
||||
76
src/interactions/types/interactions.ts
Normal file
76
src/interactions/types/interactions.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Embed } from "./embed.ts";
|
||||
import { MemberCreatePayload } from "./member.ts";
|
||||
import { AllowedMentions } from "./misc.ts";
|
||||
|
||||
export interface Interaction {
|
||||
/** The id of the interaction */
|
||||
id: string;
|
||||
/** The type of interaction */
|
||||
type: InteractionType;
|
||||
/** The command data payload */
|
||||
data?: SlashCommandInteractionData;
|
||||
/** The id of the guild it was sent from */
|
||||
guild_id: string;
|
||||
/** The id of the channel it was sent from */
|
||||
channel_id: string;
|
||||
/** The Payload of the member it was sent from */
|
||||
member: MemberCreatePayload;
|
||||
/** The token for this interaction */
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SlashCommandInteractionData {
|
||||
/** The id of the command */
|
||||
id: string;
|
||||
/** The name of the command */
|
||||
name: string;
|
||||
/** the params and values from the user */
|
||||
options: SlashCommandInteractionDataOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommandInteractionDataOption {
|
||||
/** The name of the parammeter */
|
||||
name: string;
|
||||
/** The value of the pair */
|
||||
value?: any;
|
||||
/** Present if this option is a group or subcommand */
|
||||
options?: SlashCommandInteractionDataOption[];
|
||||
}
|
||||
|
||||
export interface InteractionResponse {
|
||||
/** The type of response */
|
||||
type: InteractionResponseType;
|
||||
/** The optional response message */
|
||||
data?: SlashCommandCallbackData;
|
||||
}
|
||||
|
||||
export interface SlashCommandCallbackData {
|
||||
/** is the response TTS */
|
||||
tts?: boolean;
|
||||
/** message content */
|
||||
content: string;
|
||||
/** supports up to 10 embeds */
|
||||
embeds?: Embed[];
|
||||
/** allowed mentions for the message */
|
||||
allowed_mentions?: AllowedMentions;
|
||||
/** acceptable values are message flags */
|
||||
flags?: number;
|
||||
}
|
||||
|
||||
export enum InteractionType {
|
||||
PING = 1,
|
||||
APPLICATION_COMMAND = 2,
|
||||
}
|
||||
|
||||
export enum InteractionResponseType {
|
||||
/** ACK a `Ping` */
|
||||
PONG = 1,
|
||||
/** ACK a command without sending a message, eating the user's input */
|
||||
ACKNOWLEDGE = 2,
|
||||
/** respond with a message, eating the user's input */
|
||||
CHANNEL_MESSAGE = 3,
|
||||
/** respond with a message, showing the user's input */
|
||||
CHANNEL_MESSAGE_WITH_SOURCE = 4,
|
||||
/** ACK a command without sending a message, showing the user's input */
|
||||
ACK_WITH_SOURCE = 5,
|
||||
}
|
||||
43
src/interactions/types/member.ts
Normal file
43
src/interactions/types/member.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface UserPayload {
|
||||
/** The user's id */
|
||||
id: string;
|
||||
/** the user's username, not unique across the platform */
|
||||
username: string;
|
||||
/** The user's 4 digit discord tag */
|
||||
discriminator: string;
|
||||
/** The user's avatar hash */
|
||||
avatar: string | null;
|
||||
/** Whether the user is a bot */
|
||||
bot?: boolean;
|
||||
/** Whether the user is an official discord system user (part of the urgent message system.) */
|
||||
system?: boolean;
|
||||
/** Whether the user has two factor enabled on their account */
|
||||
mfa_enabled?: boolean;
|
||||
/** the user's chosen language option */
|
||||
locale?: string;
|
||||
/** Whether the email on this account has been verified */
|
||||
verified?: boolean;
|
||||
/** The user's email */
|
||||
email?: string;
|
||||
/** The flags on a user's account. */
|
||||
flags?: number;
|
||||
/** The type of Nitro subscription on a user's account. */
|
||||
premium_type?: number;
|
||||
}
|
||||
|
||||
export interface MemberCreatePayload {
|
||||
/** The user this guild member represents */
|
||||
user: UserPayload;
|
||||
/** The user's guild nickname if one is set. */
|
||||
nick?: string;
|
||||
/** Array of role ids that the member has */
|
||||
roles: string[];
|
||||
/** When the user joined the guild. */
|
||||
joined_at: string;
|
||||
/** When the user used their nitro boost on the server. */
|
||||
premium_since?: string;
|
||||
/** Whether the user is deafened in voice channels */
|
||||
deaf: boolean;
|
||||
/** Whether the user is muted in voice channels */
|
||||
mute: boolean;
|
||||
}
|
||||
8
src/interactions/types/misc.ts
Normal file
8
src/interactions/types/misc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface AllowedMentions {
|
||||
/** An array of allowed mention types to parse from the content. */
|
||||
parse: ("roles" | "users" | "everyone")[];
|
||||
/** Array of role_ids to mention (Max size of 100) */
|
||||
roles?: string[];
|
||||
/** Array of user_ids to mention (Max size of 100) */
|
||||
users?: string[];
|
||||
}
|
||||
6
src/interactions/types/mod.ts
Normal file
6
src/interactions/types/mod.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./embed.ts";
|
||||
export * from "./interactions.ts";
|
||||
export * from "./member.ts";
|
||||
export * from "./misc.ts";
|
||||
export * from "./slash.ts";
|
||||
export * from "./webhook.ts";
|
||||
86
src/interactions/types/slash.ts
Normal file
86
src/interactions/types/slash.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
SlashCommandCallbackData,
|
||||
} from "./interactions.ts";
|
||||
|
||||
export interface CreateSlashCommandOptions {
|
||||
/** The name of the slash command. */
|
||||
name: string;
|
||||
/** The description of the slash command. */
|
||||
description: String;
|
||||
/** If a guildID is provided, this will be a GUILD command. If none is provided it will be a GLOBAL command. */
|
||||
guildID?: string;
|
||||
/** The options for this command */
|
||||
options?: SlashCommandOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommand {
|
||||
/** unique id of the command */
|
||||
id: string;
|
||||
/** unique id of the parent application */
|
||||
application_id: string;
|
||||
/** 3-32 character name */
|
||||
name: string;
|
||||
/** 1-100 character description */
|
||||
description: string;
|
||||
/** the parameters for the command */
|
||||
options?: SlashCommandOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommandOption {
|
||||
/** The type of option */
|
||||
type: SlashCommandOptionType;
|
||||
/** 1-32 character name */
|
||||
name: string;
|
||||
/** 1-100 character description*/
|
||||
description: string;
|
||||
/** the first `required` option for the user to complete--only one option can be `default` */
|
||||
default?: boolean;
|
||||
/** if the parameter is required or optional--default `false`*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* If you specify `choices` for an option, they are the **only** valid values for a user to pick.
|
||||
* choices for `string` and `int` types for the user to pick from
|
||||
*/
|
||||
choices?: SlashCommandOptionChoice[];
|
||||
/** if the option is a subcommand or subcommand group type, this nested options will be the parameters */
|
||||
options?: SlashCommandOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommandOptionChoice {
|
||||
/** The name of the choice */
|
||||
name: string;
|
||||
/** The value of the choice */
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export enum SlashCommandOptionType {
|
||||
SUB_COMMAND = 1,
|
||||
SUB_COMMAND_GROUP = 2,
|
||||
STRING = 3,
|
||||
INTEGER = 4,
|
||||
BOOLEAN = 5,
|
||||
USER = 6,
|
||||
CHANNEL = 7,
|
||||
ROLE = 8,
|
||||
}
|
||||
|
||||
export interface EditSlashCommandOptions {
|
||||
id: string;
|
||||
guildID?: string;
|
||||
}
|
||||
|
||||
export interface ExecuteSlashCommandOptions {
|
||||
type: InteractionResponseType;
|
||||
data: SlashCommandCallbackData;
|
||||
}
|
||||
|
||||
export interface EditSlashResponseOptions extends SlashCommandCallbackData {
|
||||
/** If this is not provided, it will default to editing the original response. */
|
||||
messageID?: string;
|
||||
}
|
||||
|
||||
export interface UpsertSlashCommandOptions {
|
||||
id: string;
|
||||
guildID?: string;
|
||||
}
|
||||
27
src/interactions/types/webhook.ts
Normal file
27
src/interactions/types/webhook.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Embed } from "./embed.ts";
|
||||
|
||||
export interface ExecuteWebhookOptions {
|
||||
/** waits for server confirmation of message send before response, and returns the created message body (defaults to false; when false a message that is not saved does not return an error) */
|
||||
wait?: boolean;
|
||||
/** the message contents (up to 2000 characters) */
|
||||
content?: string;
|
||||
/** override the default username of the webhook */
|
||||
username?: string;
|
||||
/** override the default avatar of the webhook*/
|
||||
avatar_url?: string;
|
||||
/** true if this is a TTS message */
|
||||
tts?: boolean;
|
||||
/** file contents the contents of the file being sent one of content, file, embeds */
|
||||
file?: { blob: unknown; name: string };
|
||||
/** array of up to 10 embed objects embedded rich content. */
|
||||
embeds?: Embed[];
|
||||
/** allowed mentions for the message */
|
||||
mentions?: {
|
||||
/** An array of allowed mention types to parse from the content. */
|
||||
parse: ("roles" | "users" | "everyone")[];
|
||||
/** Array of role_ids to mention (Max size of 100) */
|
||||
roles?: string[];
|
||||
/** Array of user_ids to mention (Max size of 100) */
|
||||
users?: string[];
|
||||
};
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { DiscordBotGatewayData } from "../types/discord.ts";
|
||||
import { ClientOptions, EventHandlers } from "../types/options.ts";
|
||||
import { endpoints } from "../utils/constants.ts";
|
||||
import { RequestManager } from "./requestManager.ts";
|
||||
import { spawnShards } from "./shardingManager.ts";
|
||||
|
||||
export let authorization = "";
|
||||
export let botID = "";
|
||||
|
||||
export let eventHandlers: EventHandlers = {};
|
||||
|
||||
export let botGatewayData: DiscordBotGatewayData;
|
||||
|
||||
export const identifyPayload: IdentifyPayload = {
|
||||
token: "",
|
||||
compress: true,
|
||||
properties: {
|
||||
$os: "linux",
|
||||
$browser: "Discordeno",
|
||||
$device: "Discordeno",
|
||||
},
|
||||
intents: 0,
|
||||
shard: [0, 0],
|
||||
};
|
||||
|
||||
export interface IdentifyPayload {
|
||||
token: string;
|
||||
compress: boolean;
|
||||
properties: {
|
||||
$os: string;
|
||||
$browser: string;
|
||||
$device: string;
|
||||
};
|
||||
intents: number;
|
||||
shard: [number, number];
|
||||
}
|
||||
|
||||
export const createClient = async (data: ClientOptions) => {
|
||||
if (data.eventHandlers) eventHandlers = data.eventHandlers;
|
||||
authorization = `Bot ${data.token}`;
|
||||
|
||||
// Initial API connection to get info about bots connection
|
||||
botGatewayData = await RequestManager.get(
|
||||
endpoints.GATEWAY_BOT,
|
||||
) as DiscordBotGatewayData;
|
||||
|
||||
identifyPayload.token = data.token;
|
||||
identifyPayload.intents = data.intents.reduce(
|
||||
(bits, next) => (bits |= next),
|
||||
0,
|
||||
);
|
||||
identifyPayload.shard = [0, botGatewayData.shards];
|
||||
|
||||
spawnShards(botGatewayData, identifyPayload);
|
||||
};
|
||||
|
||||
export default createClient;
|
||||
|
||||
export function updateEventHandlers(newEventHandlers: EventHandlers) {
|
||||
eventHandlers = newEventHandlers;
|
||||
}
|
||||
|
||||
export function setBotID(id: string) {
|
||||
if (botID !== id) botID = id;
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
import {
|
||||
connectWebSocket,
|
||||
delay,
|
||||
isWebSocketCloseEvent,
|
||||
WebSocket,
|
||||
} from "../../deps.ts";
|
||||
import {
|
||||
DiscordBotGatewayData,
|
||||
DiscordHeartbeatPayload,
|
||||
GatewayOpcode,
|
||||
ReadyPayload,
|
||||
} from "../types/discord.ts";
|
||||
import { FetchMembersOptions } from "../types/guild.ts";
|
||||
import { DebugArg } from "../types/options.ts";
|
||||
|
||||
let shardSocket: WebSocket;
|
||||
|
||||
/** The session id is needed for RESUME functionality when discord disconnects randomly. */
|
||||
let sessionID = "";
|
||||
|
||||
// Discord requests null if no number has yet been sent by discord
|
||||
let previousSequenceNumber: number | null = null;
|
||||
let needToResume = false;
|
||||
let shardID = 0;
|
||||
|
||||
const RequestMembersQueue: RequestMemberQueuedRequest[] = [];
|
||||
let processQueue = false;
|
||||
|
||||
interface RequestMemberQueuedRequest {
|
||||
guildID: string;
|
||||
nonce: string;
|
||||
options?: FetchMembersOptions;
|
||||
}
|
||||
|
||||
async function processRequestMembersQueue() {
|
||||
if (!RequestMembersQueue.length) {
|
||||
processQueue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 2 events per second is the rate limit.
|
||||
const request = RequestMembersQueue.shift();
|
||||
if (request) {
|
||||
requestGuildMembers(request.guildID, request.nonce, request.options, true);
|
||||
|
||||
const secondRequest = RequestMembersQueue.shift();
|
||||
if (secondRequest) {
|
||||
requestGuildMembers(
|
||||
secondRequest.guildID,
|
||||
secondRequest.nonce,
|
||||
secondRequest.options,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await delay(1500);
|
||||
|
||||
postDebug(
|
||||
{
|
||||
type: "requestMembersProcessing",
|
||||
data: { shardID, remaining: RequestMembersQueue.length },
|
||||
},
|
||||
);
|
||||
processRequestMembersQueue();
|
||||
}
|
||||
|
||||
// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume.
|
||||
async function sendConstantHeartbeats(
|
||||
interval: number,
|
||||
) {
|
||||
await delay(interval);
|
||||
shardSocket.send(
|
||||
JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }),
|
||||
);
|
||||
postDebug(
|
||||
{ type: "heartbeat", data: { interval, previousSequenceNumber, shardID } },
|
||||
);
|
||||
|
||||
sendConstantHeartbeats(interval);
|
||||
}
|
||||
|
||||
async function resumeConnection(
|
||||
botGatewayData: DiscordBotGatewayData,
|
||||
identifyPayload: object,
|
||||
) {
|
||||
postDebug({ type: "resuming", data: { shardID } });
|
||||
// Run it once
|
||||
createShard(botGatewayData, identifyPayload, true);
|
||||
// Then retry every 15 seconds
|
||||
await delay(1000 * 15);
|
||||
if (needToResume) resumeConnection(botGatewayData, identifyPayload);
|
||||
}
|
||||
|
||||
const createShard = async (
|
||||
botGatewayData: DiscordBotGatewayData,
|
||||
identifyPayload: object,
|
||||
resuming = false,
|
||||
) => {
|
||||
postDebug({ type: "createShard", data: { shardID } });
|
||||
|
||||
shardSocket = await connectWebSocket(botGatewayData.url);
|
||||
let resumeInterval = 0;
|
||||
|
||||
if (!resuming) {
|
||||
// Intial identify with the gateway
|
||||
await shardSocket.send(
|
||||
JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }),
|
||||
);
|
||||
} else {
|
||||
await shardSocket.send(JSON.stringify({
|
||||
op: GatewayOpcode.Resume,
|
||||
d: {
|
||||
...identifyPayload,
|
||||
session_id: sessionID,
|
||||
seq: previousSequenceNumber,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
for await (const message of shardSocket) {
|
||||
if (typeof message === "string") {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
switch (data.op) {
|
||||
case GatewayOpcode.Hello:
|
||||
sendConstantHeartbeats(
|
||||
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
|
||||
);
|
||||
break;
|
||||
case GatewayOpcode.Reconnect:
|
||||
case GatewayOpcode.InvalidSession:
|
||||
// When d is false we need to reidentify
|
||||
if (!data.d) {
|
||||
postDebug({ type: "invalidSession", data: { shardID } });
|
||||
createShard(botGatewayData, identifyPayload);
|
||||
break;
|
||||
}
|
||||
needToResume = true;
|
||||
resumeConnection(botGatewayData, identifyPayload);
|
||||
break;
|
||||
default:
|
||||
if (data.t === "RESUMED") {
|
||||
postDebug({ type: "resumed", data: { shardID } });
|
||||
|
||||
needToResume = false;
|
||||
break;
|
||||
}
|
||||
// Important for RESUME
|
||||
if (data.t === "READY") {
|
||||
sessionID = (data.d as ReadyPayload).session_id;
|
||||
}
|
||||
|
||||
// Update the sequence number if it is present
|
||||
if (data.s) previousSequenceNumber = data.s;
|
||||
|
||||
// @ts-ignore
|
||||
postMessage(
|
||||
{
|
||||
type: "HANDLE_DISCORD_PAYLOAD",
|
||||
payload: message,
|
||||
resumeInterval,
|
||||
shardID,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if (isWebSocketCloseEvent(message)) {
|
||||
postDebug({ type: "websocketClose", data: { shardID, message } });
|
||||
|
||||
// These error codes should just crash the projects
|
||||
if ([4004, 4005, 4012, 4013, 4014].includes(message.code)) {
|
||||
console.error(`Close :( ${JSON.stringify(message)}`);
|
||||
postDebug({ type: "websocketErrored", data: { shardID, message } });
|
||||
|
||||
throw new Error(
|
||||
"Shard.ts: Error occurred that is not resumeable or able to be reconnected.",
|
||||
);
|
||||
}
|
||||
// These error codes can not be resumed but need to reconnect from start
|
||||
if ([4003, 4007, 4008, 4009].includes(message.code)) {
|
||||
postDebug(
|
||||
{ type: "websocketReconnecting", data: { shardID, message } },
|
||||
);
|
||||
createShard(botGatewayData, identifyPayload);
|
||||
} else {
|
||||
needToResume = true;
|
||||
resumeConnection(botGatewayData, identifyPayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function requestGuildMembers(
|
||||
guildID: string,
|
||||
nonce: string,
|
||||
options?: FetchMembersOptions,
|
||||
queuedRequest = false,
|
||||
) {
|
||||
// This request was not from this queue so we add it to queue first
|
||||
if (!queuedRequest) {
|
||||
RequestMembersQueue.push({
|
||||
guildID,
|
||||
nonce,
|
||||
options,
|
||||
});
|
||||
|
||||
if (!processQueue) {
|
||||
processQueue = true;
|
||||
processRequestMembersQueue();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If its closed add back to queue to redo on resume
|
||||
if (shardSocket.isClosed) {
|
||||
requestGuildMembers(guildID, nonce, options);
|
||||
return;
|
||||
}
|
||||
|
||||
shardSocket.send(JSON.stringify({
|
||||
op: GatewayOpcode.RequestGuildMembers,
|
||||
d: {
|
||||
guild_id: guildID,
|
||||
query: options?.query || "",
|
||||
limit: options?.query || 0,
|
||||
presences: options?.presences || false,
|
||||
user_ids: options?.userIDs,
|
||||
nonce,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: Errors need to be fixed by VSC plugin
|
||||
// @ts-ignore
|
||||
postMessage({ type: "REQUEST_CLIENT_OPTIONS" });
|
||||
// @ts-ignore
|
||||
onmessage = (message: MessageEvent) => {
|
||||
if (message.data.type === "CREATE_SHARD") {
|
||||
createShard(
|
||||
message.data.botGatewayData,
|
||||
message.data.identifyPayload,
|
||||
);
|
||||
shardID = message.data.shardID;
|
||||
}
|
||||
|
||||
if (message.data.type === "FETCH_MEMBERS") {
|
||||
requestGuildMembers(
|
||||
message.data.guildID,
|
||||
message.data.nonce,
|
||||
message.data.options,
|
||||
);
|
||||
}
|
||||
|
||||
if (message.data.type === "EDIT_BOTS_STATUS") {
|
||||
shardSocket.send(JSON.stringify({
|
||||
op: GatewayOpcode.StatusUpdate,
|
||||
d: {
|
||||
since: null,
|
||||
game: message.data.game.name
|
||||
? {
|
||||
name: message.data.game.name,
|
||||
type: message.data.game.type,
|
||||
}
|
||||
: null,
|
||||
status: message.data.status,
|
||||
afk: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
function postDebug(details: DebugArg) {
|
||||
// TODO: Errors need to be fixed by VSC plugin
|
||||
postMessage({ type: "DEBUG_LOG", details });
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import { delay } from "../../deps.ts";
|
||||
import { controllers } from "../controllers/mod.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import {
|
||||
DiscordBotGatewayData,
|
||||
DiscordPayload,
|
||||
GatewayOpcode,
|
||||
} from "../types/discord.ts";
|
||||
import { FetchMembersOptions } from "../types/guild.ts";
|
||||
import { cache } from "../utils/cache.ts";
|
||||
import { BotStatusRequest } from "../utils/utils.ts";
|
||||
import {
|
||||
botGatewayStatusRequest,
|
||||
createBasicShard,
|
||||
requestGuildMembers,
|
||||
} from "./basicShard.ts";
|
||||
import {
|
||||
botGatewayData,
|
||||
eventHandlers,
|
||||
IdentifyPayload,
|
||||
identifyPayload,
|
||||
} from "./client.ts";
|
||||
|
||||
let shardCounter = 0;
|
||||
let basicSharding = false;
|
||||
|
||||
const shards: Worker[] = [];
|
||||
let createNextShard = true;
|
||||
|
||||
/** This function is meant to be used on the ready event to alert the library to start the next shard. */
|
||||
export function allowNextShard(enabled = true) {
|
||||
createNextShard = enabled;
|
||||
}
|
||||
|
||||
export function createShardWorker(shardID?: number) {
|
||||
const path = new URL("./shard.ts", import.meta.url).toString();
|
||||
const shard = new Worker(path, { type: "module", deno: true });
|
||||
shard.onmessage = (message) => {
|
||||
if (message.data.type === "REQUEST_CLIENT_OPTIONS") {
|
||||
identifyPayload.shard = [
|
||||
shardID || shardCounter,
|
||||
botGatewayData.shards,
|
||||
];
|
||||
|
||||
shard.postMessage(
|
||||
{
|
||||
type: "CREATE_SHARD",
|
||||
botGatewayData,
|
||||
identifyPayload,
|
||||
shardID: shardCounter,
|
||||
},
|
||||
);
|
||||
// Update the shard counter
|
||||
shardCounter++;
|
||||
} else if (message.data.type === "HANDLE_DISCORD_PAYLOAD") {
|
||||
handleDiscordPayload(
|
||||
JSON.parse(message.data.payload),
|
||||
message.data.shardID,
|
||||
);
|
||||
} else if (message.data.type === "DEBUG_LOG") {
|
||||
eventHandlers.debug?.(message.data.details);
|
||||
}
|
||||
};
|
||||
shards.push(shard);
|
||||
}
|
||||
|
||||
export const spawnShards = async (
|
||||
data: DiscordBotGatewayData,
|
||||
payload: IdentifyPayload,
|
||||
id = 1,
|
||||
) => {
|
||||
if ((data.shards === 1 && id === 1) || id <= data.shards) {
|
||||
if (createNextShard) {
|
||||
createNextShard = false;
|
||||
if (data.shards >= 25) createShardWorker();
|
||||
else {
|
||||
basicSharding = true;
|
||||
createBasicShard(data, payload, false, id - 1);
|
||||
}
|
||||
spawnShards(data, payload, id + 1);
|
||||
} else {
|
||||
await delay(1000);
|
||||
spawnShards(data, payload, id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function handleDiscordPayload(
|
||||
data: DiscordPayload,
|
||||
shardID: number,
|
||||
) {
|
||||
eventHandlers.raw?.(data);
|
||||
await eventHandlers.dispatchRequirements?.(data, shardID);
|
||||
|
||||
switch (data.op) {
|
||||
case GatewayOpcode.HeartbeatACK:
|
||||
// Incase the user wants to listen to heartbeat responses
|
||||
return eventHandlers.heartbeat?.();
|
||||
case GatewayOpcode.Dispatch:
|
||||
if (!data.t) return;
|
||||
// Run the appropriate controller for this event.
|
||||
return controllers[data.t]?.(data, shardID);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestAllMembers(
|
||||
guild: Guild,
|
||||
resolve: Function,
|
||||
options?: FetchMembersOptions,
|
||||
) {
|
||||
const nonce = `${guild.id}-${Math.random().toString()}`;
|
||||
cache.fetchAllMembersProcessingRequests.set(nonce, resolve);
|
||||
|
||||
if (basicSharding) {
|
||||
return requestGuildMembers(guild.id, guild.shardID, nonce, options);
|
||||
}
|
||||
|
||||
shards[guild.shardID].postMessage({
|
||||
type: "FETCH_MEMBERS",
|
||||
guildID: guild.id,
|
||||
nonce,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
export function sendGatewayCommand(type: "EDIT_BOTS_STATUS", payload: object) {
|
||||
if (basicSharding) {
|
||||
if (type === "EDIT_BOTS_STATUS") {
|
||||
botGatewayStatusRequest(payload as BotStatusRequest);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
shards.forEach((shard) => {
|
||||
shard.postMessage({
|
||||
type,
|
||||
...payload,
|
||||
});
|
||||
});
|
||||
}
|
||||
1
src/rest/mod.ts
Normal file
1
src/rest/mod.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./request_manager.ts";
|
||||
@@ -1,9 +1,13 @@
|
||||
import { delay } from "../../deps.ts";
|
||||
import { HttpResponseCode } from "../types/discord.ts";
|
||||
import { Errors } from "../types/errors.ts";
|
||||
import { RequestMethods } from "../types/fetch.ts";
|
||||
import { baseEndpoints } from "../utils/constants.ts";
|
||||
import { authorization, eventHandlers } from "./client.ts";
|
||||
import { authorization, eventHandlers } from "../bot.ts";
|
||||
import { Errors, HttpResponseCode, RequestMethods } from "../types/mod.ts";
|
||||
import {
|
||||
API_VERSION,
|
||||
BASE_URL,
|
||||
baseEndpoints,
|
||||
IMAGE_BASE_URL,
|
||||
USER_AGENT,
|
||||
} from "../util/constants.ts";
|
||||
import { delay } from "../util/utils.ts";
|
||||
|
||||
const pathQueues: { [key: string]: QueuedRequest[] } = {};
|
||||
const ratelimitedPaths = new Map<string, RateLimitedPath>();
|
||||
@@ -64,8 +68,7 @@ async function cleanupQueues() {
|
||||
}
|
||||
|
||||
async function processQueue() {
|
||||
// Putting this code inside a function like this allows us to use tail recursion like a while loop without hitting the max stack error.
|
||||
async function avoidMaxStackError() {
|
||||
while (queueInProcess) {
|
||||
if (
|
||||
(Object.keys(pathQueues).length) && !globallyRateLimited
|
||||
) {
|
||||
@@ -112,12 +115,9 @@ async function processQueue() {
|
||||
}
|
||||
|
||||
if (Object.keys(pathQueues).length) {
|
||||
avoidMaxStackError();
|
||||
cleanupQueues();
|
||||
} else queueInProcess = false;
|
||||
}
|
||||
|
||||
return avoidMaxStackError();
|
||||
}
|
||||
|
||||
processRateLimitedPaths();
|
||||
@@ -143,8 +143,7 @@ export const RequestManager = {
|
||||
function createRequestBody(body: any, method: RequestMethods) {
|
||||
const headers: { [key: string]: string } = {
|
||||
Authorization: authorization,
|
||||
"User-Agent":
|
||||
`DiscordBot (https://github.com/skillz4killz/discordeno, 6.0.0)`,
|
||||
"User-Agent": USER_AGENT,
|
||||
};
|
||||
|
||||
if (method === "get") body = undefined;
|
||||
@@ -195,7 +194,7 @@ async function runMethod(
|
||||
) {
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "requestManager",
|
||||
type: "requestCreate",
|
||||
data: { method, url, body, retryCount, bucketID },
|
||||
},
|
||||
);
|
||||
@@ -203,6 +202,20 @@ async function runMethod(
|
||||
const errorStack = new Error("Location:");
|
||||
Error.captureStackTrace(errorStack);
|
||||
|
||||
// For proxies we don't need to do any of the legwork so we just forward the request
|
||||
if (
|
||||
!url.startsWith(`${BASE_URL}/v${API_VERSION}`) &&
|
||||
!url.startsWith(IMAGE_BASE_URL)
|
||||
) {
|
||||
return fetch(url, { method, body: body ? JSON.stringify(body) : undefined })
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw errorStack;
|
||||
});
|
||||
}
|
||||
|
||||
// No proxy so we need to handl all rate limiting and such
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = async () => {
|
||||
try {
|
||||
@@ -221,14 +234,14 @@ async function runMethod(
|
||||
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "requestManagerFetching",
|
||||
type: "requestFetch",
|
||||
data: { method, url, body, retryCount, bucketID },
|
||||
},
|
||||
);
|
||||
const response = await fetch(urlToUse, createRequestBody(body, method));
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "requestManagerFetched",
|
||||
type: "requestFetched",
|
||||
data: { method, url, body, retryCount, bucketID, response },
|
||||
},
|
||||
);
|
||||
@@ -262,7 +275,7 @@ async function runMethod(
|
||||
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "requestManagerSuccess",
|
||||
type: "requestSuccess",
|
||||
data: { method, url, body, retryCount, bucketID },
|
||||
},
|
||||
);
|
||||
@@ -1,52 +0,0 @@
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { ChannelCreatePayload } from "../types/channel.ts";
|
||||
import { Unpromise } from "../types/misc.ts";
|
||||
import { calculatePermissions } from "../utils/permissions.ts";
|
||||
|
||||
export async function createChannel(
|
||||
data: ChannelCreatePayload,
|
||||
guildID?: string,
|
||||
) {
|
||||
const {
|
||||
guild_id: rawGuildID,
|
||||
last_message_id: lastMessageID,
|
||||
user_limit: userLimit,
|
||||
rate_limit_per_user: rateLimitPerUser,
|
||||
parent_id: parentID,
|
||||
last_pin_timestamp: lastPinTimestamp,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const channel = {
|
||||
...rest,
|
||||
/** The guild id of the channel if it is a guild channel. */
|
||||
guildID: guildID || rawGuildID || "",
|
||||
/** The id of the last message sent in this channel */
|
||||
lastMessageID,
|
||||
/** The amount of users allowed in this voice channel. */
|
||||
userLimit,
|
||||
/** The rate limit(slowmode) in this text channel that users can send messages. */
|
||||
rateLimitPerUser,
|
||||
/** The category id for this channel */
|
||||
parentID,
|
||||
/** The last time when a message was pinned in this channel */
|
||||
lastPinTimestamp,
|
||||
/** The permission overwrites for this channel */
|
||||
permissions: data.permission_overwrites
|
||||
? data.permission_overwrites.map((perm) => ({
|
||||
...perm,
|
||||
allow: calculatePermissions(BigInt(perm.allow)),
|
||||
deny: calculatePermissions(BigInt(perm.deny)),
|
||||
}))
|
||||
: [],
|
||||
/** Whether this channel is nsfw or not */
|
||||
nsfw: data.nsfw || false,
|
||||
/** The mention of the channel */
|
||||
mention: `<#${data.id}>`,
|
||||
};
|
||||
|
||||
cacheHandlers.set("channels", data.id, channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
export interface Channel extends Unpromise<ReturnType<typeof createChannel>> {}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { CreateGuildPayload } from "../types/guild.ts";
|
||||
import { Unpromise } from "../types/misc.ts";
|
||||
import { Collection } from "../utils/collection.ts";
|
||||
import { Member } from "./member.ts";
|
||||
import { structures } from "./mod.ts";
|
||||
|
||||
export async function createGuild(data: CreateGuildPayload, shardID: number) {
|
||||
const {
|
||||
owner_id: ownerID,
|
||||
afk_channel_id: afkChannelID,
|
||||
afk_timeout: afkTimeout,
|
||||
widget_enabled: widgetEnabled,
|
||||
widget_channel_id: widgetChannelID,
|
||||
verification_level: verificationLevel,
|
||||
mfa_level: mfaLevel,
|
||||
system_channel_id: systemChannelID,
|
||||
max_presences: maxPresences,
|
||||
max_members: maxMembers,
|
||||
vanity_url_code: vanityURLCode,
|
||||
premium_tier: premiumTier,
|
||||
premium_subscription_count: premiumSubscriptionCount,
|
||||
preferred_locale: preferredLocale,
|
||||
joined_at: joinedAt,
|
||||
member_count: memberCount,
|
||||
voice_states: voiceStates,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const roles = await Promise.all(
|
||||
data.roles.map((r) => structures.createRole(r)),
|
||||
);
|
||||
const channels = await Promise.all(
|
||||
data.channels.map((c) => structures.createChannel(c, data.id)),
|
||||
);
|
||||
|
||||
const guild = {
|
||||
...rest,
|
||||
/** The shard id that this guild is on */
|
||||
shardID,
|
||||
/** The owner id of the guild. */
|
||||
ownerID,
|
||||
/** The afk channel id for this guild. */
|
||||
afkChannelID,
|
||||
/** The amount of time before a user is moved to AFK. */
|
||||
afkTimeout,
|
||||
/** Whether or not the embed is enabled in this server. */
|
||||
widgetEnabled,
|
||||
/** The channel id for the guild embed in this server. */
|
||||
widgetChannelID,
|
||||
/** The verification level for this server. */
|
||||
verificationLevel,
|
||||
/** The MFA level for this server. */
|
||||
mfaLevel,
|
||||
/** The system channel id for this server. */
|
||||
systemChannelID,
|
||||
/** The max presences for this server. */
|
||||
maxPresences,
|
||||
/** The maximum members in this server. */
|
||||
maxMembers,
|
||||
/** The vanity URL code for this server. */
|
||||
vanityURLCode,
|
||||
/** The premium tier for this server. */
|
||||
premiumTier,
|
||||
/** The subscription count for this server. */
|
||||
premiumSubscriptionCount,
|
||||
/** The preferred language in this server. */
|
||||
preferredLocale,
|
||||
|
||||
/** The roles in the guild */
|
||||
roles: new Collection(roles.map((r) => [r.id, r])),
|
||||
/** When this guild was joined at. */
|
||||
joinedAt: Date.parse(joinedAt),
|
||||
/** The users in this guild. */
|
||||
members: new Collection<string, Member>(),
|
||||
/** The channels in the guild */
|
||||
channels: new Collection(channels.map((c) => [c.id, c])),
|
||||
/** The presences of all the users in the guild. */
|
||||
presences: new Collection(data.presences.map((p) => [p.user.id, p])),
|
||||
/** The total number of members in this guild. This value is updated as members leave and join the server. However, if you do not have the intent enabled to be able to listen to these events, then this will not be accurate. */
|
||||
memberCount: memberCount || 0,
|
||||
/** The Voice State data for each user in a voice channel in this server. */
|
||||
voiceStates: new Collection(voiceStates.map((vs) => [vs.user_id, {
|
||||
...vs,
|
||||
guildID: vs.guild_id,
|
||||
channelID: vs.channel_id,
|
||||
userID: vs.user_id,
|
||||
sessionID: vs.session_id,
|
||||
selfDeaf: vs.self_deaf,
|
||||
selfMute: vs.self_mute,
|
||||
selfStream: vs.self_stream,
|
||||
}])),
|
||||
};
|
||||
|
||||
data.members.forEach(async (m) =>
|
||||
guild.members.set(m.user.id, await structures.createMember(m, guild.id))
|
||||
);
|
||||
|
||||
return guild;
|
||||
}
|
||||
|
||||
export interface Guild extends Unpromise<ReturnType<typeof createGuild>> {}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { MemberCreatePayload } from "../types/member.ts";
|
||||
import { Unpromise } from "../types/misc.ts";
|
||||
|
||||
export async function createMember(data: MemberCreatePayload, guildID: string) {
|
||||
const {
|
||||
joined_at: joinedAt,
|
||||
premium_since: premiumSince,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const {
|
||||
mfa_enabled: mfaEnabled,
|
||||
premium_type: premiumType,
|
||||
...user
|
||||
} = data.user || {};
|
||||
|
||||
const member = {
|
||||
...rest,
|
||||
// Only use those that we have not removed above
|
||||
user: user,
|
||||
/** When the user joined the guild */
|
||||
joinedAt: Date.parse(joinedAt),
|
||||
/** When the user used their nitro boost on the server. */
|
||||
premiumSince: premiumSince ? Date.parse(premiumSince) : undefined,
|
||||
/** The guild id where this member exists */
|
||||
guildID,
|
||||
/** Whether or not this user has 2FA enabled. */
|
||||
mfaEnabled,
|
||||
/** The premium type for this user */
|
||||
premiumType,
|
||||
};
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
export interface Member extends Unpromise<ReturnType<typeof createMember>> {}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { MessageCreateOptions } from "../types/message.ts";
|
||||
import { Unpromise } from "../types/misc.ts";
|
||||
|
||||
export async function createMessage(data: MessageCreateOptions) {
|
||||
const {
|
||||
guild_id: guildID,
|
||||
channel_id: channelID,
|
||||
mentions_everyone: mentionsEveryone,
|
||||
mention_channels: mentionChannels,
|
||||
mention_roles: mentionRoles,
|
||||
webhook_id: webhookID,
|
||||
message_reference: messageReference,
|
||||
edited_timestamp: editedTimestamp,
|
||||
referenced_message: referencedMessageID,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const message = {
|
||||
...rest,
|
||||
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
|
||||
referencedMessageID,
|
||||
channelID,
|
||||
guildID: guildID || "",
|
||||
mentions: data.mentions.map((m) => m.id),
|
||||
mentionsEveryone,
|
||||
mentionRoles,
|
||||
mentionChannels: mentionChannels || [],
|
||||
webhookID,
|
||||
messageReference,
|
||||
timestamp: Date.parse(data.timestamp),
|
||||
editedTimestamp: editedTimestamp ? Date.parse(editedTimestamp) : undefined,
|
||||
};
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export interface Message extends Unpromise<ReturnType<typeof createMessage>> {}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Unpromise } from "../types/misc.ts";
|
||||
import { RoleData } from "../types/role.ts";
|
||||
|
||||
export async function createRole(data: RoleData) {
|
||||
const { tags, ...rest } = data;
|
||||
|
||||
const roleTags = {
|
||||
botID: tags?.bot_id,
|
||||
premiumSubscriber: "premium_subscriber" in (tags ?? {}),
|
||||
integrationID: tags?.integration_id,
|
||||
};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
/** The @ mention of the role in a string. */
|
||||
mention: `<@&${data.id}>`,
|
||||
tags: roleTags,
|
||||
};
|
||||
}
|
||||
|
||||
export interface Role extends Unpromise<ReturnType<typeof createRole>> {}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { GuildTemplate } from "../types/guild.ts";
|
||||
|
||||
export function createTemplate(
|
||||
data: GuildTemplate,
|
||||
) {
|
||||
const {
|
||||
usage_count: usageCount,
|
||||
creator_id: creatorID,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
source_guild_id: sourceGuildID,
|
||||
serialized_source_guild: serializedSourceGuild,
|
||||
is_dirty: isDirty,
|
||||
...rest
|
||||
} = data;
|
||||
|
||||
const template = {
|
||||
...rest,
|
||||
usageCount,
|
||||
creatorID,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
sourceGuildID,
|
||||
serializedSourceGuild,
|
||||
isDirty,
|
||||
};
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
export interface Template extends ReturnType<typeof createTemplate> {}
|
||||
@@ -120,6 +120,15 @@ export interface MessageContent {
|
||||
replyMessageID?: string;
|
||||
}
|
||||
|
||||
export interface AllowedMentions {
|
||||
/** An array of allowed mention types to parse from the content. */
|
||||
parse: ("roles" | "users" | "everyone")[];
|
||||
/** Array of role_ids to mention (Max size of 100) */
|
||||
roles?: string[];
|
||||
/** Array of user_ids to mention (Max size of 100) */
|
||||
users?: string[];
|
||||
}
|
||||
|
||||
export interface GetMessages {
|
||||
/** Max number of messages to return(1-100). Defaults to 50. */
|
||||
limit?: number;
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface DiscordPayload {
|
||||
s?: number;
|
||||
/** The event name for this payload. ONLY for OPCode 0 */
|
||||
t?:
|
||||
| "READY"
|
||||
| "APPLICATION_COMMAND_CREATE"
|
||||
| "CHANNEL_CREATE"
|
||||
| "CHANNEL_DELETE"
|
||||
| "CHANNEL_UPDATE"
|
||||
@@ -29,6 +29,7 @@ export interface DiscordPayload {
|
||||
| "GUILD_ROLE_CREATE"
|
||||
| "GUILD_ROLE_DELETE"
|
||||
| "GUILD_ROLE_UPDATE"
|
||||
| "INTERACTION_CREATE"
|
||||
| "MESSAGE_CREATE"
|
||||
| "MESSAGE_DELETE"
|
||||
| "MESSAGE_DELETE_BULK"
|
||||
@@ -38,6 +39,7 @@ export interface DiscordPayload {
|
||||
| "MESSAGE_REACTION_REMOVE_ALL"
|
||||
| "MESSAGE_REACTION_REMOVE_EMOJI"
|
||||
| "PRESENCE_UPDATE"
|
||||
| "READY"
|
||||
| "TYPING_START"
|
||||
| "USER_UPDATE"
|
||||
| "VOICE_STATE_UPDATE"
|
||||
@@ -57,6 +59,11 @@ export interface DiscordBotGatewayData {
|
||||
remaining: number;
|
||||
/** Milliseconds left until limit is reset. */
|
||||
reset_after: number;
|
||||
/** The number of identify requests allowed per 5 seconds.
|
||||
* So, if you had a max concurrency of 16, and 16 shards for example, you could start them all up at the same time.
|
||||
* Whereas if you had 32 shards, if you tried to start up shard 0 and 16 at the same time for example, it would not work. You can start shards 0-15 concurrently, then 16-31...
|
||||
* */
|
||||
max_concurrency: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export enum Errors {
|
||||
BOTS_HIGHEST_ROLE_TOO_LOW = "BOTS_HIGHEST_ROLE_TOO_LOW",
|
||||
CHANNEL_NOT_IN_GUILD = "CHANNEL_NOT_IN_GUILD",
|
||||
INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME",
|
||||
INVALID_SLASH_NAME = "INVALID_SLASH_NAME",
|
||||
INVALID_SLASH_DESCRIPTION = "INVALID_SLASH_DESCRIPTION",
|
||||
INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS",
|
||||
CHANNEL_NOT_FOUND = "CHANNEL_NOT_FOUND",
|
||||
CHANNEL_NOT_TEXT_BASED = "CHANNEL_NOT_TEXT_BASED",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Guild } from "../api/structures/mod.ts";
|
||||
import { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
|
||||
import { Emoji, StatusType } from "./discord.ts";
|
||||
import { MemberCreatePayload } from "./member.ts";
|
||||
@@ -469,9 +469,15 @@ export interface RawOverwrite {
|
||||
/** Whether this is a role or a member */
|
||||
type: OverwriteType;
|
||||
/** The permissions that this id is allowed to do. (This will mark it as a green check.) */
|
||||
allow: number;
|
||||
allow: string;
|
||||
/** The permissions that this id is NOT allowed to do. (This will mark it as a red x.) */
|
||||
deny: number;
|
||||
deny: string;
|
||||
}
|
||||
|
||||
export interface PermissionOverwrite
|
||||
extends Omit<RawOverwrite, "allow" | "deny"> {
|
||||
allow: Permission[];
|
||||
deny: Permission[];
|
||||
}
|
||||
|
||||
export interface ChannelCreateOptions {
|
||||
@@ -488,7 +494,7 @@ export interface ChannelCreateOptions {
|
||||
/** The sorting position of the channel */
|
||||
position?: number;
|
||||
/** The channel's permission overwrites */
|
||||
permission_overwrites?: Overwrite[];
|
||||
permissionOverwrites?: Overwrite[];
|
||||
/** The id of the parent category for the channel */
|
||||
parent_id?: string;
|
||||
/** Whether the channel is nsfw */
|
||||
|
||||
43
src/types/interactions.ts
Normal file
43
src/types/interactions.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { MemberCreatePayload } from "./member.ts";
|
||||
|
||||
export interface InteractionCommandPayload {
|
||||
/** id of the interaction */
|
||||
id: string;
|
||||
/** the type of interaction */
|
||||
type: InteractionType;
|
||||
/** the command data payload */
|
||||
data?: InteractionData;
|
||||
/** the guild it was sent from */
|
||||
guild_id: string;
|
||||
/** the channel it was sent from */
|
||||
channel_id: string;
|
||||
/** guild member data for the invoking user */
|
||||
member: MemberCreatePayload;
|
||||
/** a contintuation token for responding to the interaction */
|
||||
token: string;
|
||||
}
|
||||
|
||||
export enum InteractionType {
|
||||
/** This type is for ACK on webhook only setup. Discord may send these which require. In a sense its a heartbeat. */
|
||||
PING = 1,
|
||||
/** Slash commands */
|
||||
APPLICATION_COMMAND,
|
||||
}
|
||||
|
||||
export interface InteractionData {
|
||||
/** the ID of the invoked command */
|
||||
id: string;
|
||||
/** the name of the invoked command */
|
||||
name: string;
|
||||
/** the params + values from the user */
|
||||
options: InteractionDataOption[];
|
||||
}
|
||||
|
||||
export interface InteractionDataOption {
|
||||
/** the name of the parameter */
|
||||
name: string;
|
||||
/** the value of the pair. present if there was no more options */
|
||||
value?: string | number;
|
||||
/** present if this option is a group or subcommand */
|
||||
options?: InteractionDataOption[];
|
||||
}
|
||||
@@ -28,4 +28,23 @@ export interface MemberCreatePayload {
|
||||
deaf: boolean;
|
||||
/** Whether the user is muted in voice channels */
|
||||
mute: boolean;
|
||||
/** Whether the user has passed the guild's Membership Screening requirements */
|
||||
pending?: boolean;
|
||||
}
|
||||
|
||||
export interface GuildMember {
|
||||
/** The user's guild nickname if one is set. */
|
||||
nick?: string;
|
||||
/** Array of role ids that the member has */
|
||||
roles: string[];
|
||||
/** When the user joined the guild. */
|
||||
joinedAt: number;
|
||||
/** When the user used their nitro boost on the server. */
|
||||
premiumSince?: number;
|
||||
/** Whether the user is deafened in voice channels */
|
||||
deaf: boolean;
|
||||
/** Whether the user is muted in voice channels */
|
||||
mute: boolean;
|
||||
/** Whether the user has passed the guild's Membership Screening requirements */
|
||||
pending?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Channel } from "../structures/channel.ts";
|
||||
import { Channel } from "../api/structures/mod.ts";
|
||||
import { ChannelType } from "./channel.ts";
|
||||
import { UserPayload } from "./guild.ts";
|
||||
import { MemberCreatePayload } from "./member.ts";
|
||||
@@ -280,6 +280,8 @@ export interface MessageCreateOptions {
|
||||
message_reference?: Reference;
|
||||
/** The message flags combined like permission bits describe extra features of the message */
|
||||
flags?: 1 | 2 | 4 | 8 | 16;
|
||||
/** the stickers sent with the message (bots currently can only receive messages with stickers, not send) */
|
||||
stickers?: MessageSticker[];
|
||||
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
|
||||
referenced_message?: MessageCreateOptions | null;
|
||||
}
|
||||
@@ -342,3 +344,28 @@ export interface PartialMessage {
|
||||
id: string;
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
export interface MessageSticker {
|
||||
/** id of the sticker */
|
||||
id: string;
|
||||
/** id of the pack the sticker is from */
|
||||
pack_id: string;
|
||||
/** name of the sticker */
|
||||
name: string;
|
||||
/** description of the sticker */
|
||||
description: string;
|
||||
/** a comma-separated list of tags for the sticker */
|
||||
tags?: string;
|
||||
/** sticker asset hash. The URL for fetching sticker assets is currently private. */
|
||||
asset: string;
|
||||
/** sticker preview asset hash. The URL for fetching sticker assets is currently private. */
|
||||
preview_asset: string | null;
|
||||
/** type of sticker format */
|
||||
format_type: MessageStickerFormat;
|
||||
}
|
||||
|
||||
export enum MessageStickerFormat {
|
||||
PNG = 1,
|
||||
APNG,
|
||||
LOTTIE,
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export type Unpromise<T extends Promise<unknown>> = T extends Promise<infer K>
|
||||
? K
|
||||
: never;
|
||||
15
src/types/mod.ts
Normal file
15
src/types/mod.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from "./activity.ts";
|
||||
export * from "./cdn.ts";
|
||||
export * from "./channel.ts";
|
||||
export * from "./discord.ts";
|
||||
export * from "./errors.ts";
|
||||
export * from "./fetch.ts";
|
||||
export * from "./guild.ts";
|
||||
export * from "./interactions.ts";
|
||||
export * from "./member.ts";
|
||||
export * from "./message.ts";
|
||||
export * from "./options.ts";
|
||||
export * from "./permission.ts";
|
||||
export * from "./presence.ts";
|
||||
export * from "./role.ts";
|
||||
export * from "./webhook.ts";
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Channel } from "../structures/channel.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Member } from "../structures/member.ts";
|
||||
import { Message } from "../structures/message.ts";
|
||||
import { Role } from "../structures/role.ts";
|
||||
import {
|
||||
Channel,
|
||||
Guild,
|
||||
Member,
|
||||
Message,
|
||||
Role,
|
||||
} from "../api/structures/mod.ts";
|
||||
import {
|
||||
DiscordPayload,
|
||||
Emoji,
|
||||
PresenceUpdatePayload,
|
||||
Properties,
|
||||
TypingStartPayload,
|
||||
VoiceStateUpdatePayload,
|
||||
} from "./discord.ts";
|
||||
@@ -22,16 +23,8 @@ import {
|
||||
ReactionPayload,
|
||||
} from "./message.ts";
|
||||
|
||||
export interface Fulfilled_Client_Options {
|
||||
export interface BotConfig {
|
||||
token: string;
|
||||
properties: Properties;
|
||||
compress: boolean;
|
||||
intents: number;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
token: string;
|
||||
properties?: Properties;
|
||||
compress?: boolean;
|
||||
intents: Intents[];
|
||||
eventHandlers?: EventHandlers;
|
||||
@@ -55,24 +48,24 @@ export interface OldMessage {
|
||||
export interface DebugArg {
|
||||
/** Red is for errors or urgent issues. Yellow is for warnings/alerts. Green is for actions being taken. Blue is for */
|
||||
type?:
|
||||
| "identifying"
|
||||
| "gatewayIdentify"
|
||||
| "error"
|
||||
| "requestManager"
|
||||
| "globallyRateLimited"
|
||||
| "requestManagerSuccess"
|
||||
| "requestManagerFetching"
|
||||
| "requestManagerFetched"
|
||||
| "requestCreate"
|
||||
| "requestSuccess"
|
||||
| "requestFetch"
|
||||
| "requestFetched"
|
||||
| "requestMembersProcessing"
|
||||
| "heartbeat"
|
||||
| "heartbeatStopped"
|
||||
| "createShard"
|
||||
| "invalidSession"
|
||||
| "reconnect"
|
||||
| "resuming"
|
||||
| "resumed"
|
||||
| "websocketClose"
|
||||
| "websocketErrored"
|
||||
| "websocketReconnecting"
|
||||
| "gatewayHeartbeat"
|
||||
| "gatewayHeartbeatStopped"
|
||||
| "shardCreate"
|
||||
| "gatewayInvalidSession"
|
||||
| "gatewayReconnect"
|
||||
| "gatewayResume"
|
||||
| "gatewayResumed"
|
||||
| "wsClose"
|
||||
| "wsError"
|
||||
| "wsReconnect"
|
||||
| "missingShard";
|
||||
data: unknown;
|
||||
}
|
||||
@@ -84,8 +77,12 @@ export interface EventHandlers {
|
||||
channelDelete?: (channel: Channel) => unknown;
|
||||
debug?: (args: DebugArg) => unknown;
|
||||
dispatchRequirements?: (data: DiscordPayload, shardID: number) => unknown;
|
||||
guildBanAdd?: (guild: Guild, user: Member | UserPayload) => unknown;
|
||||
guildBanRemove?: (guild: Guild, user: Member | UserPayload) => unknown;
|
||||
guildBanAdd?: (guild: Guild, user: UserPayload, member?: Member) => unknown;
|
||||
guildBanRemove?: (
|
||||
guild: Guild,
|
||||
user: UserPayload,
|
||||
member?: Member,
|
||||
) => unknown;
|
||||
guildCreate?: (guild: Guild) => unknown;
|
||||
guildLoaded?: (guild: Guild) => unknown;
|
||||
guildUpdate?: (guild: Guild, changes: GuildUpdateChange[]) => unknown;
|
||||
@@ -96,15 +93,21 @@ export interface EventHandlers {
|
||||
cachedEmojis: Emoji[],
|
||||
) => unknown;
|
||||
guildMemberAdd?: (guild: Guild, member: Member) => unknown;
|
||||
guildMemberRemove?: (guild: Guild, member: Member | UserPayload) => unknown;
|
||||
guildMemberRemove?: (
|
||||
guild: Guild,
|
||||
user: UserPayload,
|
||||
member?: Member,
|
||||
) => unknown;
|
||||
guildMemberUpdate?: (
|
||||
guild: Guild,
|
||||
member: Member,
|
||||
cachedMember?: Member,
|
||||
) => unknown;
|
||||
heartbeat?: () => unknown;
|
||||
// TODO: FIX THIS
|
||||
interactionCreate?: (data: unknown) => unknown;
|
||||
messageCreate?: (message: Message) => unknown;
|
||||
messageDelete?: (message: Message | PartialMessage) => unknown;
|
||||
messageDelete?: (partial: PartialMessage, message?: Message) => unknown;
|
||||
messageUpdate?: (message: Message, cachedMessage: OldMessage) => unknown;
|
||||
nicknameUpdate?: (
|
||||
guild: Guild,
|
||||
@@ -120,14 +123,16 @@ export interface EventHandlers {
|
||||
rawGateway?: (data: unknown) => unknown;
|
||||
ready?: () => unknown;
|
||||
reactionAdd?: (
|
||||
message: Message | MessageReactionUncachedPayload,
|
||||
payload: MessageReactionUncachedPayload,
|
||||
emoji: ReactionPayload,
|
||||
userID: string,
|
||||
message?: Message,
|
||||
) => unknown;
|
||||
reactionRemove?: (
|
||||
message: Message | MessageReactionUncachedPayload,
|
||||
payload: MessageReactionUncachedPayload,
|
||||
emoji: ReactionPayload,
|
||||
userID: string,
|
||||
message?: Message,
|
||||
) => unknown;
|
||||
reactionRemoveAll?: (data: BaseMessageReactionPayload) => unknown;
|
||||
reactionRemoveEmoji?: (data: MessageReactionRemoveEmojiPayload) => unknown;
|
||||
@@ -152,6 +157,7 @@ export interface EventHandlers {
|
||||
webhooksUpdate?: (channelID: string, guildID: string) => unknown;
|
||||
}
|
||||
|
||||
/** https://discord.com/developers/docs/topics/gateway#list-of-intents */
|
||||
export enum Intents {
|
||||
/** Enables the following events:
|
||||
* - GUILD_CREATE
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AllowedMentions } from "./channel.ts";
|
||||
import { UserPayload } from "./guild.ts";
|
||||
import { InteractionType } from "./interactions.ts";
|
||||
import { Embed } from "./message.ts";
|
||||
|
||||
export interface WebhookPayload {
|
||||
@@ -59,3 +61,159 @@ export interface ExecuteWebhookOptions {
|
||||
users?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface EditWebhookMessageOptions {
|
||||
content?: string;
|
||||
embeds?: Embed[];
|
||||
allowed_mentions?: AllowedMentions;
|
||||
}
|
||||
|
||||
export interface CreateSlashCommandOptions {
|
||||
/** The name of the slash command. */
|
||||
name: string;
|
||||
/** The description of the slash command. */
|
||||
description: String;
|
||||
/** If a guildID is provided, this will be a GUILD command. If none is provided it will be a GLOBAL command. */
|
||||
guildID?: string;
|
||||
/** The options for this command */
|
||||
options?: SlashCommandOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommand {
|
||||
/** unique id of the command */
|
||||
id: string;
|
||||
/** unique id of the parent application */
|
||||
application_id: string;
|
||||
/** 3-32 character name */
|
||||
name: string;
|
||||
/** 1-100 character description */
|
||||
description: string;
|
||||
/** the parameters for the command */
|
||||
options?: SlashCommandOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommandOption {
|
||||
/** The type of option */
|
||||
type: SlashCommandOptionType;
|
||||
/** 1-32 character name */
|
||||
name: string;
|
||||
/** 1-100 character description*/
|
||||
description: string;
|
||||
/** the first `required` option for the user to complete--only one option can be `default` */
|
||||
default?: boolean;
|
||||
/** if the parameter is required or optional--default `false`*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* If you specify `choices` for an option, they are the **only** valid values for a user to pick.
|
||||
* choices for `string` and `int` types for the user to pick from
|
||||
*/
|
||||
choices?: SlashCommandOptionChoice[];
|
||||
/** if the option is a subcommand or subcommand group type, this nested options will be the parameters */
|
||||
options?: SlashCommandOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommandOptionChoice {
|
||||
/** The name of the choice */
|
||||
name: string;
|
||||
/** The value of the choice */
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export enum SlashCommandOptionType {
|
||||
SUB_COMMAND = 1,
|
||||
SUB_COMMAND_GROUP = 2,
|
||||
STRING = 3,
|
||||
INTEGER = 4,
|
||||
BOOLEAN = 5,
|
||||
USER = 6,
|
||||
CHANNEL = 7,
|
||||
ROLE = 8,
|
||||
}
|
||||
|
||||
export interface Interaction {
|
||||
/** The id of the interaction */
|
||||
id: string;
|
||||
/** The type of interaction */
|
||||
type: InteractionType;
|
||||
/** The command data payload */
|
||||
data?: SlashCommandInteractionData;
|
||||
/** The id of the guild it was sent from */
|
||||
guild_id: string;
|
||||
/** The id of the channel it was sent from */
|
||||
channel_id: string;
|
||||
/** The Payload of the member it was sent from */
|
||||
member: UserPayload;
|
||||
/** The token for this interaction */
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SlashCommandInteractionData {
|
||||
/** The id of the command */
|
||||
id: string;
|
||||
/** The name of the command */
|
||||
name: string;
|
||||
/** the params and values from the user */
|
||||
options: SlashCommandInteractionDataOption[];
|
||||
}
|
||||
|
||||
export interface SlashCommandInteractionDataOption {
|
||||
/** The name of the parammeter */
|
||||
name: string;
|
||||
/** The value of the pair */
|
||||
value?: any;
|
||||
/** Present if this option is a group or subcommand */
|
||||
options?: SlashCommandInteractionDataOption[];
|
||||
}
|
||||
|
||||
export interface InteractionResponse {
|
||||
/** The type of response */
|
||||
type: InteractionResponseType;
|
||||
/** The optional response message */
|
||||
data?: SlashCommandCallbackData;
|
||||
}
|
||||
|
||||
export interface SlashCommandCallbackData {
|
||||
/** is the response TTS */
|
||||
tts?: boolean;
|
||||
/** message content */
|
||||
content: string;
|
||||
/** supports up to 10 embeds */
|
||||
embeds?: Embed[];
|
||||
/** allowed mentions for the message */
|
||||
allowed_mentions?: AllowedMentions;
|
||||
/** acceptable values are message flags */
|
||||
flags?: number;
|
||||
}
|
||||
|
||||
export enum InteractionResponseType {
|
||||
/** ACK a `Ping` */
|
||||
PONG = 1,
|
||||
/** ACK a command without sending a message, eating the user's input */
|
||||
ACKNOWLEDGE = 2,
|
||||
/** respond with a message, eating the user's input */
|
||||
CHANNEL_MESSAGE = 3,
|
||||
/** respond with a message, showing the user's input */
|
||||
CHANNEL_MESSAGE_WITH_SOURCE = 4,
|
||||
/** ACK a command without sending a message, showing the user's input */
|
||||
ACK_WITH_SOURCE = 5,
|
||||
}
|
||||
|
||||
export interface EditSlashCommandOptions {
|
||||
id: string;
|
||||
guildID?: string;
|
||||
}
|
||||
|
||||
export interface ExecuteSlashCommandOptions {
|
||||
type: InteractionResponseType;
|
||||
data: SlashCommandCallbackData;
|
||||
}
|
||||
|
||||
export interface EditSlashResponseOptions extends SlashCommandCallbackData {
|
||||
/** If this is not provided, it will default to editing the original response. */
|
||||
messageID?: string;
|
||||
}
|
||||
|
||||
export interface UpsertSlashCommandOptions {
|
||||
id: string;
|
||||
guildID?: string;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Channel } from "../structures/channel.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Message } from "../structures/message.ts";
|
||||
import { PresenceUpdatePayload } from "../types/discord.ts";
|
||||
import {
|
||||
Channel,
|
||||
Guild,
|
||||
Member,
|
||||
Message,
|
||||
} from "../api/structures/structures.ts";
|
||||
import { PresenceUpdatePayload } from "../types/mod.ts";
|
||||
import { Collection } from "./collection.ts";
|
||||
|
||||
export interface CacheData {
|
||||
@@ -9,9 +12,11 @@ export interface CacheData {
|
||||
guilds: Collection<string, Guild>;
|
||||
channels: Collection<string, Channel>;
|
||||
messages: Collection<string, Message>;
|
||||
members: Collection<string, Member>;
|
||||
unavailableGuilds: Collection<string, number>;
|
||||
presences: Collection<string, PresenceUpdatePayload>;
|
||||
fetchAllMembersProcessingRequests: Collection<string, Function>;
|
||||
executedSlashCommands: Collection<string, string>;
|
||||
}
|
||||
|
||||
export const cache: CacheData = {
|
||||
@@ -19,7 +24,9 @@ export const cache: CacheData = {
|
||||
guilds: new Collection(),
|
||||
channels: new Collection(),
|
||||
messages: new Collection(),
|
||||
members: new Collection(),
|
||||
unavailableGuilds: new Collection(),
|
||||
presences: new Collection(),
|
||||
fetchAllMembersProcessingRequests: new Collection(),
|
||||
executedSlashCommands: new Collection(),
|
||||
};
|
||||
@@ -1,15 +1,25 @@
|
||||
let API_VERSION = "v8";
|
||||
/** https://discord.com/developers/docs/reference#api-reference-base-url */
|
||||
export const BASE_URL = "https://discord.com/api";
|
||||
|
||||
/** https://discord.com/developers/docs/reference#api-versioning-api-versions */
|
||||
export const API_VERSION = 8;
|
||||
|
||||
/** https://discord.com/developers/docs/topics/gateway#gateways-gateway-versions */
|
||||
export const GATEWAY_VERSION = 8;
|
||||
|
||||
/** https://discord.com/developers/docs/reference#user-agent */
|
||||
export const USER_AGENT =
|
||||
"DiscordBot (https://github.com/discordeno/discordeno, v10)";
|
||||
|
||||
/** https://discord.com/developers/docs/reference#image-formatting-image-base-url */
|
||||
export const IMAGE_BASE_URL = "https://cdn.discordapp.com";
|
||||
|
||||
// This can be modified by big brain bots and use a proxy
|
||||
export const baseEndpoints = {
|
||||
/** Although, the version can be defaulted, keep the v6 as it can be changed to test newer versions when necessary. */
|
||||
BASE_URL: `https://discord.com/api/${API_VERSION}`,
|
||||
CDN_URL: "https://cdn.discordapp.com",
|
||||
BASE_URL: `${BASE_URL}/v${API_VERSION}`,
|
||||
CDN_URL: IMAGE_BASE_URL,
|
||||
};
|
||||
|
||||
export function changeAPIVersion(number = 7) {
|
||||
API_VERSION = `v${number}`;
|
||||
}
|
||||
|
||||
const GUILDS_BASE = (id: string) => `${baseEndpoints.BASE_URL}/guilds/${id}`;
|
||||
|
||||
export const endpoints = {
|
||||
@@ -96,6 +106,32 @@ export const endpoints = {
|
||||
WEBHOOK: (id: string, token: string) =>
|
||||
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}`,
|
||||
WEBHOOK_ID: (id: string) => `${baseEndpoints.BASE_URL}/webhooks/${id}`,
|
||||
WEBHOOK_EDIT: (id: string, token: string, messageID: string) =>
|
||||
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/${messageID}`,
|
||||
WEBHOOK_DELETE: (id: string, token: string, messageID: string) =>
|
||||
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/${messageID}`,
|
||||
|
||||
// Application Endpoints
|
||||
COMMANDS: (botID: string) =>
|
||||
`${baseEndpoints.BASE_URL}/applications/${botID}/commands`,
|
||||
COMMANDS_GUILD: (botID: string, id: string) =>
|
||||
`${baseEndpoints.BASE_URL}/applications/${botID}/guilds/${id}/commands`,
|
||||
COMMANDS_ID: (botID: string, id: string) =>
|
||||
`${baseEndpoints.BASE_URL}/applications/${botID}/commands/${id}`,
|
||||
COMMANDS_GUILD_ID: (botID: string, id: string, guildID: string) =>
|
||||
`${baseEndpoints.BASE_URL}/applications/${botID}/guilds/${guildID}/commands/${id}`,
|
||||
|
||||
// Interaction Endpoints
|
||||
INTERACTION_ID_TOKEN: (id: string, token: string) =>
|
||||
`${baseEndpoints.BASE_URL}/interactions/${id}/${token}/callback`,
|
||||
INTERACTION_ORIGINAL_ID_TOKEN: (id: string, token: string) =>
|
||||
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/@original`,
|
||||
INTERACTION_ID_TOKEN_MESSAGEID: (
|
||||
id: string,
|
||||
token: string,
|
||||
messageID: string,
|
||||
) =>
|
||||
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/${messageID}`,
|
||||
|
||||
// User endpoints
|
||||
USER: (id: string) => `${baseEndpoints.BASE_URL}/users/${id}`,
|
||||
@@ -1,9 +1,7 @@
|
||||
import { cacheHandlers } from "../controllers/cache.ts";
|
||||
import { botID } from "../module/client.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Role } from "../structures/role.ts";
|
||||
import { RawOverwrite } from "../types/guild.ts";
|
||||
import { Permission, Permissions } from "../types/permission.ts";
|
||||
import { cacheHandlers } from "../api/controllers/cache.ts";
|
||||
import { Guild, Role } from "../api/structures/structures.ts";
|
||||
import { botID } from "../bot.ts";
|
||||
import { Permission, Permissions, RawOverwrite } from "../types/mod.ts";
|
||||
|
||||
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
|
||||
export async function memberIDHasPermission(
|
||||
@@ -16,10 +14,12 @@ export async function memberIDHasPermission(
|
||||
|
||||
if (memberID === guild.ownerID) return true;
|
||||
|
||||
const member = guild.members.get(memberID);
|
||||
const member = (await cacheHandlers.get("members", memberID))?.guilds.get(
|
||||
guildID,
|
||||
);
|
||||
if (!member) return false;
|
||||
|
||||
return memberHasPermission(member.guildID, guild, member.roles, permissions);
|
||||
return memberHasPermission(memberID, guild, member.roles, permissions);
|
||||
}
|
||||
|
||||
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
|
||||
@@ -50,7 +50,7 @@ export function memberHasPermission(
|
||||
|
||||
export async function botHasPermission(
|
||||
guildID: string,
|
||||
permissions: Permissions[],
|
||||
permissions: Permission[],
|
||||
) {
|
||||
const guild = await cacheHandlers.get("guilds", guildID);
|
||||
if (!guild) return false;
|
||||
@@ -58,7 +58,9 @@ export async function botHasPermission(
|
||||
// Check if the bot is the owner of the guild, if it is, returns true
|
||||
if (guild.ownerID === botID) return true;
|
||||
|
||||
const member = guild.members.get(botID);
|
||||
const member = (await cacheHandlers.get("members", botID))?.guilds.get(
|
||||
guildID,
|
||||
);
|
||||
if (!member) return false;
|
||||
|
||||
// The everyone role is not in member.roles
|
||||
@@ -74,13 +76,15 @@ export async function botHasPermission(
|
||||
|
||||
if (permissionBits & BigInt(Permissions.ADMINISTRATOR)) return true;
|
||||
|
||||
return permissions.every((permission) => permissionBits & BigInt(permission));
|
||||
return permissions.every((permission) =>
|
||||
permissionBits & BigInt(Permissions[permission])
|
||||
);
|
||||
}
|
||||
|
||||
/** Checks if the bot has the permissions in a channel */
|
||||
export function botHasChannelPermissions(
|
||||
channelID: string,
|
||||
permissions: Permissions[],
|
||||
permissions: Permission[],
|
||||
) {
|
||||
return hasChannelPermissions(channelID, botID, permissions);
|
||||
}
|
||||
@@ -89,7 +93,7 @@ export function botHasChannelPermissions(
|
||||
export async function hasChannelPermissions(
|
||||
channelID: string,
|
||||
memberID: string,
|
||||
permissions: Permissions[],
|
||||
permissions: Permission[],
|
||||
) {
|
||||
const channel = await cacheHandlers.get("channels", channelID);
|
||||
if (!channel) return false;
|
||||
@@ -104,15 +108,16 @@ export async function hasChannelPermissions(
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const member = guild.members.get(memberID);
|
||||
const member = (await cacheHandlers.get("members", memberID))?.guilds.get(
|
||||
guild.id,
|
||||
);
|
||||
if (!member) return false;
|
||||
|
||||
let memberOverwrite: RawOverwrite | undefined;
|
||||
let everyoneOverwrite: RawOverwrite | undefined;
|
||||
let rolesOverwrites: RawOverwrite[] = [];
|
||||
|
||||
for (const overwrite of channel.permission_overwrites || []) {
|
||||
for (const overwrite of channel.permissionOverwrites || []) {
|
||||
// If the overwrite on this channel is specific to this member
|
||||
if (overwrite.id === memberID) memberOverwrite = overwrite;
|
||||
// If it is the everyone role overwrite
|
||||
@@ -121,17 +126,20 @@ export async function hasChannelPermissions(
|
||||
if (member.roles.includes(overwrite.id)) rolesOverwrites.push(overwrite);
|
||||
}
|
||||
|
||||
const allowedPermissions = new Set<Permissions>();
|
||||
const allowedPermissions = new Set<Permission>();
|
||||
|
||||
// Member perms override everything so we must check them first
|
||||
if (memberOverwrite) {
|
||||
const allowBits = memberOverwrite.allow;
|
||||
const denyBits = memberOverwrite.deny;
|
||||
for (const perm of permissions) {
|
||||
// One of the necessary permissions is denied. Since this is main permission we can cancel if its denied.
|
||||
if (BigInt(memberOverwrite.deny) & BigInt(perm)) return false;
|
||||
if (BigInt(denyBits) & BigInt(Permissions[perm])) return false;
|
||||
// Already allowed perm
|
||||
if (allowedPermissions.has(perm)) continue;
|
||||
|
||||
// This perm is allowed so we save it
|
||||
if (BigInt(memberOverwrite.allow) & BigInt(perm)) {
|
||||
if (BigInt(allowBits) & BigInt(Permissions[perm])) {
|
||||
allowedPermissions.add(perm);
|
||||
}
|
||||
}
|
||||
@@ -143,17 +151,19 @@ export async function hasChannelPermissions(
|
||||
if (allowedPermissions.has(perm)) continue;
|
||||
|
||||
for (const overwrite of rolesOverwrites) {
|
||||
const allowBits = overwrite.allow;
|
||||
// This perm is allowed so we save it
|
||||
if (BigInt(overwrite.allow) & BigInt(perm)) {
|
||||
if (BigInt(allowBits) & BigInt(Permissions[perm])) {
|
||||
allowedPermissions.add(perm);
|
||||
break;
|
||||
}
|
||||
|
||||
const denyBits = overwrite.deny;
|
||||
// If this role denies it we need to save and check if another role allows it, allows > deny
|
||||
if (BigInt(overwrite.deny) & BigInt(perm)) {
|
||||
if (BigInt(denyBits) & BigInt(Permissions[perm])) {
|
||||
// This role denies his perm, but before denying we need to check all other roles if any allow as allow > deny
|
||||
const isAllowed = rolesOverwrites.some((o) =>
|
||||
BigInt(o.allow) & BigInt(perm)
|
||||
BigInt(o.allow) & BigInt(Permissions[perm])
|
||||
);
|
||||
if (isAllowed) continue;
|
||||
// This permission is in fact denied. Since Roles overrule everything below here we can cancel ou here
|
||||
@@ -163,13 +173,15 @@ export async function hasChannelPermissions(
|
||||
}
|
||||
|
||||
if (everyoneOverwrite) {
|
||||
const allowBits = everyoneOverwrite.allow;
|
||||
const denyBits = everyoneOverwrite.deny;
|
||||
for (const perm of permissions) {
|
||||
// Already allowed perm
|
||||
if (allowedPermissions.has(perm)) continue;
|
||||
// One of the necessary permissions is denied. Since everyone overwrite overrides role perms we can cancel here
|
||||
if (BigInt(everyoneOverwrite.deny) & BigInt(perm)) return false;
|
||||
if (BigInt(denyBits) & BigInt(Permissions[perm])) return false;
|
||||
// This perm is allowed so we save it
|
||||
if (BigInt(everyoneOverwrite.allow) & BigInt(perm)) {
|
||||
if (BigInt(allowBits) & BigInt(Permissions[perm])) {
|
||||
allowedPermissions.add(perm);
|
||||
}
|
||||
}
|
||||
@@ -179,14 +191,14 @@ export async function hasChannelPermissions(
|
||||
if (permissions.every((perm) => allowedPermissions.has(perm))) return true;
|
||||
|
||||
// Some permission was not explicitly allowed so we default to checking role perms directly
|
||||
const hasPerms = await botHasPermission(guild.id, permissions);
|
||||
const hasPerms = await memberIDHasPermission(memberID, guild.id, permissions);
|
||||
return hasPerms;
|
||||
}
|
||||
|
||||
/** This function converts a bitwise string to permission strings */
|
||||
export function calculatePermissions(permissionBits: bigint) {
|
||||
return Object.keys(Permissions).filter((perm) => {
|
||||
if (typeof perm !== "number") return false;
|
||||
if (Number(perm)) return false;
|
||||
return permissionBits & BigInt(Permissions[perm as Permission]);
|
||||
}) as Permission[];
|
||||
}
|
||||
@@ -203,7 +215,9 @@ export async function highestRole(guildID: string, memberID: string) {
|
||||
const guild = await cacheHandlers.get("guilds", guildID);
|
||||
if (!guild) return;
|
||||
|
||||
const member = guild?.members.get(memberID);
|
||||
const member = (await cacheHandlers.get("members", memberID))?.guilds.get(
|
||||
guildID,
|
||||
);
|
||||
if (!member) return;
|
||||
|
||||
let memberHighestRole: Role | undefined;
|
||||
@@ -1,7 +1,11 @@
|
||||
import { encode } from "../../deps.ts";
|
||||
import { sendGatewayCommand } from "../module/shardingManager.ts";
|
||||
import { ActivityType } from "../types/activity.ts";
|
||||
import { StatusType } from "../types/discord.ts";
|
||||
import {
|
||||
ActivityType,
|
||||
ImageFormats,
|
||||
ImageSize,
|
||||
StatusType,
|
||||
} from "../types/mod.ts";
|
||||
import { sendGatewayCommand } from "../ws/shard_manager.ts";
|
||||
|
||||
export const sleep = (timeout: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
@@ -33,3 +37,25 @@ export async function urlToBase64(url: string) {
|
||||
const type = url.substring(url.lastIndexOf(".") + 1);
|
||||
return `data:image/${type};base64,${imageStr}`;
|
||||
}
|
||||
|
||||
/** Allows easy way to add a prop to a base object when needing to use complicated getters solution. */
|
||||
export function createNewProp(value: any): Partial<PropertyDescriptor> {
|
||||
return { configurable: true, enumerable: true, writable: true, value };
|
||||
}
|
||||
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise((res): number =>
|
||||
setTimeout((): void => {
|
||||
res();
|
||||
}, ms)
|
||||
);
|
||||
}
|
||||
|
||||
export const formatImageURL = (
|
||||
url: string,
|
||||
size: ImageSize = 128,
|
||||
format?: ImageFormats,
|
||||
) => {
|
||||
return `${url}.${format ||
|
||||
(url.includes("/a_") ? "gif" : "jpg")}?size=${size}`;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ImageFormats, ImageSize } from "../types/cdn.ts";
|
||||
|
||||
export const formatImageURL = (
|
||||
url: string,
|
||||
size: ImageSize = 128,
|
||||
format?: ImageFormats,
|
||||
) => {
|
||||
return `${url}.${format ||
|
||||
(url.includes("/a_") ? "gif" : "jpg")}?size=${size}`;
|
||||
};
|
||||
1
src/ws/deps.ts
Normal file
1
src/ws/deps.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { decompress_with as decompressWith } from "https://unpkg.com/@evan/wasm@0.0.25/target/zlib/deno.js";
|
||||
2
src/ws/mod.ts
Normal file
2
src/ws/mod.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./shard.ts";
|
||||
export * from "./shard_manager.ts";
|
||||
@@ -1,22 +1,19 @@
|
||||
import {
|
||||
connectWebSocket,
|
||||
delay,
|
||||
inflate,
|
||||
isWebSocketCloseEvent,
|
||||
isWebSocketPingEvent,
|
||||
isWebSocketPongEvent,
|
||||
WebSocket,
|
||||
} from "../../deps.ts";
|
||||
botGatewayData,
|
||||
eventHandlers,
|
||||
IdentifyPayload,
|
||||
proxyWSURL,
|
||||
} from "../bot.ts";
|
||||
import {
|
||||
DiscordBotGatewayData,
|
||||
DiscordHeartbeatPayload,
|
||||
FetchMembersOptions,
|
||||
GatewayOpcode,
|
||||
ReadyPayload,
|
||||
} from "../types/discord.ts";
|
||||
import { FetchMembersOptions } from "../types/guild.ts";
|
||||
import { BotStatusRequest } from "../utils/utils.ts";
|
||||
import { botGatewayData, eventHandlers, IdentifyPayload } from "./client.ts";
|
||||
import { handleDiscordPayload } from "./shardingManager.ts";
|
||||
} from "../types/mod.ts";
|
||||
import { BotStatusRequest, delay } from "../util/utils.ts";
|
||||
import { decompressWith } from "./deps.ts";
|
||||
import { handleDiscordPayload } from "./shard_manager.ts";
|
||||
|
||||
const basicShards = new Map<number, BasicShard>();
|
||||
const heartbeating = new Map<number, boolean>();
|
||||
@@ -26,7 +23,7 @@ let processQueue = false;
|
||||
|
||||
export interface BasicShard {
|
||||
id: number;
|
||||
socket: WebSocket;
|
||||
ws: WebSocket;
|
||||
resumeInterval: number;
|
||||
sessionID: string;
|
||||
previousSequenceNumber: number | null;
|
||||
@@ -40,7 +37,7 @@ interface RequestMemberQueuedRequest {
|
||||
options?: FetchMembersOptions;
|
||||
}
|
||||
|
||||
export async function createBasicShard(
|
||||
export async function createShard(
|
||||
data: DiscordBotGatewayData,
|
||||
identifyPayload: IdentifyPayload,
|
||||
resuming = false,
|
||||
@@ -48,9 +45,11 @@ export async function createBasicShard(
|
||||
) {
|
||||
const oldShard = basicShards.get(shardID);
|
||||
|
||||
const ws = new WebSocket(proxyWSURL);
|
||||
ws.binaryType = "arraybuffer";
|
||||
const basicShard: BasicShard = {
|
||||
id: shardID,
|
||||
socket: await connectWebSocket(`${data.url}?v=8&encoding=json`),
|
||||
ws,
|
||||
resumeInterval: 0,
|
||||
sessionID: oldShard?.sessionID || "",
|
||||
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
|
||||
@@ -59,53 +58,26 @@ export async function createBasicShard(
|
||||
|
||||
basicShards.set(basicShard.id, basicShard);
|
||||
|
||||
if (!resuming) {
|
||||
// Intial identify with the gateway
|
||||
await identify(basicShard, identifyPayload);
|
||||
} else {
|
||||
await resume(basicShard, identifyPayload);
|
||||
}
|
||||
ws.onopen = async () => {
|
||||
if (!resuming) {
|
||||
// Initial identify with the gateway
|
||||
await identify(basicShard, identifyPayload);
|
||||
} else {
|
||||
await resume(basicShard, identifyPayload);
|
||||
}
|
||||
};
|
||||
|
||||
for await (let message of basicShard.socket) {
|
||||
if (isWebSocketCloseEvent(message)) {
|
||||
eventHandlers.debug?.(
|
||||
{ type: "websocketClose", data: { shardID: basicShard.id, message } },
|
||||
);
|
||||
ws.onerror = ({ timeStamp }) => {
|
||||
eventHandlers.debug?.({ type: "wsError", data: { timeStamp } });
|
||||
};
|
||||
|
||||
// These error codes should just crash the projects
|
||||
if ([4004, 4005, 4012, 4013, 4014].includes(message.code)) {
|
||||
console.error(`Close :( ${JSON.stringify(message)}`);
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "websocketErrored",
|
||||
data: { shardID: basicShard.id, message },
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
"Shard.ts: Error occurred that is not resumeable or able to be reconnected.",
|
||||
);
|
||||
}
|
||||
// These error codes can not be resumed but need to reconnect from start
|
||||
if ([4003, 4007, 4008, 4009].includes(message.code)) {
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "websocketReconnecting",
|
||||
data: { shardID: basicShard.id, message },
|
||||
},
|
||||
);
|
||||
createBasicShard(botGatewayData, identifyPayload, false, shardID);
|
||||
} else {
|
||||
basicShard.needToResume = true;
|
||||
resumeConnection(botGatewayData, identifyPayload, basicShard.id);
|
||||
}
|
||||
continue;
|
||||
} else if (isWebSocketPingEvent(message) || isWebSocketPongEvent(message)) {
|
||||
continue;
|
||||
ws.onmessage = ({ data: message }) => {
|
||||
if (message instanceof ArrayBuffer) {
|
||||
message = new Uint8Array(message);
|
||||
}
|
||||
|
||||
if (message instanceof Uint8Array) {
|
||||
message = inflate(
|
||||
message = decompressWith(
|
||||
message,
|
||||
0,
|
||||
(slice: Uint8Array) => utf8decoder.decode(slice),
|
||||
@@ -122,6 +94,7 @@ export async function createBasicShard(
|
||||
basicShard,
|
||||
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
|
||||
identifyPayload,
|
||||
data,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -130,27 +103,30 @@ export async function createBasicShard(
|
||||
break;
|
||||
case GatewayOpcode.Reconnect:
|
||||
eventHandlers.debug?.(
|
||||
{ type: "reconnect", data: { shardID: basicShard.id } },
|
||||
{ type: "gatewayReconnect", data: { shardID: basicShard.id } },
|
||||
);
|
||||
basicShard.needToResume = true;
|
||||
resumeConnection(botGatewayData, identifyPayload, basicShard.id);
|
||||
resumeConnection(data, identifyPayload, basicShard.id);
|
||||
break;
|
||||
case GatewayOpcode.InvalidSession:
|
||||
eventHandlers.debug?.(
|
||||
{ type: "invalidSession", data: { shardID: basicShard.id, data } },
|
||||
{
|
||||
type: "gatewayInvalidSession",
|
||||
data: { shardID: basicShard.id, data },
|
||||
},
|
||||
);
|
||||
// When d is false we need to reidentify
|
||||
if (!data.d) {
|
||||
createBasicShard(botGatewayData, identifyPayload, false, shardID);
|
||||
createShard(data, identifyPayload, false, shardID);
|
||||
break;
|
||||
}
|
||||
basicShard.needToResume = true;
|
||||
resumeConnection(botGatewayData, identifyPayload, basicShard.id);
|
||||
resumeConnection(data, identifyPayload, basicShard.id);
|
||||
break;
|
||||
default:
|
||||
if (data.t === "RESUMED") {
|
||||
eventHandlers.debug?.(
|
||||
{ type: "resumed", data: { shardID: basicShard.id } },
|
||||
{ type: "gatewayResumed", data: { shardID: basicShard.id } },
|
||||
);
|
||||
|
||||
basicShard.needToResume = false;
|
||||
@@ -168,20 +144,80 @@ export async function createBasicShard(
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = ({ reason, code, wasClean }) => {
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "wsClose",
|
||||
data: { shardID: basicShard.id, code, reason, wasClean },
|
||||
},
|
||||
);
|
||||
|
||||
switch (code) {
|
||||
case 4001:
|
||||
throw new Error(
|
||||
"[Unknown opcode] Sent an invalid Gateway opcode or an invalid payload for an opcode.",
|
||||
);
|
||||
case 4002:
|
||||
throw new Error("[Decode error] Sent an invalid payload to API.");
|
||||
case 4004:
|
||||
throw new Error(
|
||||
"[Authentication failed] The account token sent with your identify payload is incorrect.",
|
||||
);
|
||||
case 4005:
|
||||
throw new Error(
|
||||
"[Already authenticated] Sent more than one identify payload.",
|
||||
);
|
||||
case 4010:
|
||||
throw new Error(
|
||||
"[Invalid shard] Sent an invalid shard when identifying.",
|
||||
);
|
||||
case 4011:
|
||||
throw new Error(
|
||||
"[Sharding required] The session would have handled too many guilds - you are required to shard your connection in order to connect.",
|
||||
);
|
||||
case 4012:
|
||||
throw new Error(
|
||||
"[Invalid API version] Sent an invalid version for the gateway.",
|
||||
);
|
||||
case 4013:
|
||||
throw new Error(
|
||||
"[Invalid intent(s)] Sent an invalid intent for a Gateway Intent.",
|
||||
);
|
||||
case 4014:
|
||||
throw new Error(
|
||||
"[Disallowed intent(s)] Sent a disallowed intent for a Gateway Intent. You may have tried to specify an intent that you have not enabled or are not whitelisted for.",
|
||||
);
|
||||
case 4003:
|
||||
case 4007:
|
||||
case 4008:
|
||||
case 4009:
|
||||
eventHandlers.debug?.({
|
||||
type: "wsReconnect",
|
||||
data: { shardID: basicShard.id, code, reason, wasClean },
|
||||
});
|
||||
createShard(data, identifyPayload, false, shardID);
|
||||
break;
|
||||
default:
|
||||
basicShard.needToResume = true;
|
||||
resumeConnection(botGatewayData, identifyPayload, shardID);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function identify(shard: BasicShard, payload: IdentifyPayload) {
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "identifying",
|
||||
type: "gatewayIdentify",
|
||||
data: {
|
||||
shardID: shard.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return shard.socket.send(
|
||||
return shard.ws.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
op: GatewayOpcode.Identify,
|
||||
@@ -192,7 +228,7 @@ function identify(shard: BasicShard, payload: IdentifyPayload) {
|
||||
}
|
||||
|
||||
function resume(shard: BasicShard, payload: IdentifyPayload) {
|
||||
return shard.socket.send(JSON.stringify({
|
||||
return shard.ws.send(JSON.stringify({
|
||||
op: GatewayOpcode.Resume,
|
||||
d: {
|
||||
token: payload.token,
|
||||
@@ -206,11 +242,12 @@ async function heartbeat(
|
||||
shard: BasicShard,
|
||||
interval: number,
|
||||
payload: IdentifyPayload,
|
||||
data: DiscordBotGatewayData,
|
||||
) {
|
||||
// We lost socket connection between heartbeats, resume connection
|
||||
if (shard.socket.isClosed) {
|
||||
if (shard.ws.readyState === WebSocket.CLOSED) {
|
||||
shard.needToResume = true;
|
||||
resumeConnection(botGatewayData, payload, shard.id);
|
||||
resumeConnection(data, payload, shard.id);
|
||||
heartbeating.delete(shard.id);
|
||||
return;
|
||||
}
|
||||
@@ -221,7 +258,7 @@ async function heartbeat(
|
||||
if (!receivedACK) {
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "heartbeatStopped",
|
||||
type: "gatewayHeartbeatStopped",
|
||||
data: {
|
||||
interval,
|
||||
previousSequenceNumber: shard.previousSequenceNumber,
|
||||
@@ -229,21 +266,21 @@ async function heartbeat(
|
||||
},
|
||||
},
|
||||
);
|
||||
return shard.socket.send(JSON.stringify({ op: 4009 }));
|
||||
return shard.ws.send(JSON.stringify({ op: 4009 }));
|
||||
}
|
||||
}
|
||||
|
||||
// Set it to false as we are issuing a new heartbeat
|
||||
heartbeating.set(shard.id, false);
|
||||
|
||||
shard.socket.send(
|
||||
shard.ws.send(
|
||||
JSON.stringify(
|
||||
{ op: GatewayOpcode.Heartbeat, d: shard.previousSequenceNumber },
|
||||
),
|
||||
);
|
||||
eventHandlers.debug?.(
|
||||
{
|
||||
type: "heartbeat",
|
||||
type: "gatewayHeartbeat",
|
||||
data: {
|
||||
interval,
|
||||
previousSequenceNumber: shard.previousSequenceNumber,
|
||||
@@ -252,11 +289,11 @@ async function heartbeat(
|
||||
},
|
||||
);
|
||||
await delay(interval);
|
||||
heartbeat(shard, interval, payload);
|
||||
heartbeat(shard, interval, payload, data);
|
||||
}
|
||||
|
||||
async function resumeConnection(
|
||||
botGatewayData: DiscordBotGatewayData,
|
||||
data: DiscordBotGatewayData,
|
||||
payload: IdentifyPayload,
|
||||
shardID: number,
|
||||
) {
|
||||
@@ -270,12 +307,12 @@ async function resumeConnection(
|
||||
|
||||
if (!shard.needToResume) return;
|
||||
|
||||
eventHandlers.debug?.({ type: "resuming", data: { shardID: shard.id } });
|
||||
eventHandlers.debug?.({ type: "gatewayResume", data: { shardID: shard.id } });
|
||||
// Run it once
|
||||
createBasicShard(botGatewayData, payload, true, shard.id);
|
||||
createShard(data, payload, true, shard.id);
|
||||
// Then retry every 15 seconds
|
||||
await delay(1000 * 15);
|
||||
if (shard.needToResume) resumeConnection(botGatewayData, payload, shardID);
|
||||
if (shard.needToResume) resumeConnection(data, payload, shardID);
|
||||
}
|
||||
|
||||
export function requestGuildMembers(
|
||||
@@ -304,16 +341,17 @@ export function requestGuildMembers(
|
||||
}
|
||||
|
||||
// If its closed add back to queue to redo on resume
|
||||
if (shard?.socket.isClosed) {
|
||||
if (shard?.ws.readyState === WebSocket.CLOSED) {
|
||||
requestGuildMembers(guildID, shardID, nonce, options);
|
||||
return;
|
||||
}
|
||||
|
||||
shard?.socket.send(JSON.stringify({
|
||||
shard?.ws.send(JSON.stringify({
|
||||
op: GatewayOpcode.RequestGuildMembers,
|
||||
d: {
|
||||
guild_id: guildID,
|
||||
query: options?.query || "",
|
||||
// If a query is provided use it, OR if a limit is NOT provided use ""
|
||||
query: options?.query || (options?.limit ? undefined : ""),
|
||||
limit: options?.limit || 0,
|
||||
presences: options?.presences || false,
|
||||
user_ids: options?.userIDs,
|
||||
@@ -386,7 +424,7 @@ async function processGatewayQueue() {
|
||||
|
||||
export function botGatewayStatusRequest(payload: BotStatusRequest) {
|
||||
basicShards.forEach((shard) => {
|
||||
shard.socket.send(JSON.stringify({
|
||||
shard.ws.send(JSON.stringify({
|
||||
op: GatewayOpcode.StatusUpdate,
|
||||
d: {
|
||||
since: null,
|
||||
107
src/ws/shard_manager.ts
Normal file
107
src/ws/shard_manager.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { controllers } from "../api/controllers/mod.ts";
|
||||
import { Guild } from "../api/structures/structures.ts";
|
||||
import { eventHandlers, IdentifyPayload } from "../bot.ts";
|
||||
import {
|
||||
DiscordBotGatewayData,
|
||||
DiscordPayload,
|
||||
FetchMembersOptions,
|
||||
GatewayOpcode,
|
||||
} from "../types/mod.ts";
|
||||
import { cache } from "../util/cache.ts";
|
||||
import { BotStatusRequest, delay } from "../util/utils.ts";
|
||||
import {
|
||||
botGatewayStatusRequest,
|
||||
createShard,
|
||||
requestGuildMembers,
|
||||
} from "./mod.ts";
|
||||
|
||||
let createNextShard = true;
|
||||
|
||||
/** This function is meant to be used on the ready event to alert the library to start the next shard. */
|
||||
export function allowNextShard(enabled = true) {
|
||||
createNextShard = enabled;
|
||||
}
|
||||
|
||||
export async function spawnShards(
|
||||
data: DiscordBotGatewayData,
|
||||
payload: IdentifyPayload,
|
||||
shardID: number,
|
||||
lastShardID: number,
|
||||
skipChecks?: number,
|
||||
) {
|
||||
// All shards on this worker have started! Cancel out.
|
||||
if (shardID >= lastShardID) return;
|
||||
|
||||
if (skipChecks) {
|
||||
payload.shard = [
|
||||
shardID,
|
||||
data.shards > lastShardID ? data.shards : lastShardID,
|
||||
];
|
||||
// Start The shard
|
||||
createShard(data, payload, false, shardID);
|
||||
// Spawn next shard
|
||||
spawnShards(
|
||||
data,
|
||||
payload,
|
||||
shardID + 1,
|
||||
lastShardID,
|
||||
skipChecks - 1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we can create a shard or we are waiting for shards to connect still.
|
||||
if (createNextShard) {
|
||||
createNextShard = false;
|
||||
// Start the next few shards based on max concurrency
|
||||
spawnShards(
|
||||
data,
|
||||
payload,
|
||||
shardID,
|
||||
lastShardID,
|
||||
data.session_start_limit.max_concurrency,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await delay(1000);
|
||||
spawnShards(data, payload, shardID, lastShardID, skipChecks);
|
||||
}
|
||||
|
||||
export async function handleDiscordPayload(
|
||||
data: DiscordPayload,
|
||||
shardID: number,
|
||||
) {
|
||||
eventHandlers.raw?.(data);
|
||||
await eventHandlers.dispatchRequirements?.(data, shardID);
|
||||
|
||||
switch (data.op) {
|
||||
case GatewayOpcode.HeartbeatACK:
|
||||
// Incase the user wants to listen to heartbeat responses
|
||||
return eventHandlers.heartbeat?.();
|
||||
case GatewayOpcode.Dispatch:
|
||||
if (!data.t) return;
|
||||
// Run the appropriate controller for this event.
|
||||
return controllers[data.t]?.(data, shardID);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestAllMembers(
|
||||
guild: Guild,
|
||||
resolve: Function,
|
||||
options?: FetchMembersOptions,
|
||||
) {
|
||||
const nonce = `${guild.id}-${Date.now()}`;
|
||||
cache.fetchAllMembersProcessingRequests.set(nonce, resolve);
|
||||
return requestGuildMembers(guild.id, guild.shardID, nonce, options);
|
||||
}
|
||||
|
||||
export function sendGatewayCommand(type: "EDIT_BOTS_STATUS", payload: object) {
|
||||
if (type === "EDIT_BOTS_STATUS") {
|
||||
botGatewayStatusRequest(payload as BotStatusRequest);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
5
tests/deps.ts
Normal file
5
tests/deps.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
assert,
|
||||
assertArrayIncludes,
|
||||
assertEquals,
|
||||
} from "https://deno.land/std@0.81.0/testing/asserts.ts";
|
||||
@@ -1,33 +1,31 @@
|
||||
import { assert, assertEquals, delay } from "../deps.ts";
|
||||
import {
|
||||
botID,
|
||||
cache,
|
||||
createClient,
|
||||
channelOverwriteHasPermission,
|
||||
createGuildChannel,
|
||||
createGuildRole,
|
||||
createServer,
|
||||
delay,
|
||||
deleteChannel,
|
||||
deleteRole,
|
||||
deleteServer,
|
||||
editChannel,
|
||||
editRole,
|
||||
getChannel,
|
||||
getMessage,
|
||||
Guild,
|
||||
Intents,
|
||||
OverwriteType,
|
||||
Role,
|
||||
sendMessage,
|
||||
startBot,
|
||||
} from "../mod.ts";
|
||||
import {
|
||||
channelOverwriteHasPermission,
|
||||
editChannel,
|
||||
} from "../src/handlers/channel.ts";
|
||||
import { getChannel } from "../src/handlers/guild.ts";
|
||||
import { Permissions } from "../src/types/permission.ts";
|
||||
import { assert, assertEquals } from "./deps.ts";
|
||||
|
||||
const token = Deno.env.get("DISCORD_TOKEN");
|
||||
if (!token) throw "Token is not provided";
|
||||
|
||||
createClient({
|
||||
startBot({
|
||||
token,
|
||||
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
|
||||
});
|
||||
@@ -41,8 +39,8 @@ const testOptions = {
|
||||
Deno.test({
|
||||
name: "connect to the gateway",
|
||||
fn: async () => {
|
||||
// Delay the execution by 15 seconds (15000 ms)
|
||||
await delay(15000);
|
||||
// Delay the execution by 10 seconds (15000 ms)
|
||||
await delay(10000);
|
||||
|
||||
// Check whether botID is nil or not
|
||||
assert(botID);
|
||||
@@ -85,8 +83,6 @@ Deno.test({
|
||||
name: "Role 1",
|
||||
});
|
||||
|
||||
console.log(createdRole);
|
||||
|
||||
// Check whether the created role is nil or not
|
||||
assert(createdRole);
|
||||
|
||||
@@ -173,19 +169,19 @@ Deno.test({
|
||||
const channel = cache.channels.get(data.channelID);
|
||||
if (!channel) throw "Channel not found";
|
||||
|
||||
if (!channel.permission_overwrites) throw "Channel overwrites not found.";
|
||||
if (!channel.permissionOverwrites) throw "Channel overwrites not found.";
|
||||
|
||||
const hasPerm = channelOverwriteHasPermission(
|
||||
data.guildID,
|
||||
data.roleID,
|
||||
channel.permission_overwrites,
|
||||
[Permissions.VIEW_CHANNEL, Permissions.SEND_MESSAGES],
|
||||
channel.permissionOverwrites,
|
||||
["VIEW_CHANNEL", "SEND_MESSAGES"],
|
||||
);
|
||||
const missingPerm = channelOverwriteHasPermission(
|
||||
data.guildID,
|
||||
data.roleID,
|
||||
channel.permission_overwrites,
|
||||
[Permissions.USE_EXTERNAL_EMOJIS],
|
||||
channel.permissionOverwrites,
|
||||
["USE_EXTERNAL_EMOJIS"],
|
||||
);
|
||||
|
||||
assertEquals(hasPerm, true);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noUnusedLocals": true /* Report errors on unused locals. */,
|
||||
"noUnusedParameters": true /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
|
||||
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user