0 ? dataset[0].bench.unit : "",
+ },
+ beginAtZero: true,
+ },
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ afterTitle: (items) => {
+ const index = items[0].dataIndex;
+ const data = dataset[index];
+ return "\n" + data.commit.message + "\n\n" + data.commit.timestamp + " committed by @" +
+ data.commit.author.username + "\n";
+ },
+ label: (item) => {
+ let label = item.formattedValue;
+ const { range, unit } = dataset[item.datasetIndex].bench;
+ label += " " + unit;
+ if (range) {
+ label += " (" + range + ")";
+ }
+ return label;
+ },
+ afterLabel: (item) => {
+ const { extra } = dataset[item.datasetIndex].bench;
+ return extra ? "\n" + extra : "";
+ },
+ },
+ },
+ },
+ onClick: (_mouseEvent, activeElems) => {
+ if (activeElems.length === 0) {
+ return;
+ }
+ // XXX: Undocumented. How can we know the index?
+ const index = activeElems[0].index;
+ const url = dataset[index].commit.url;
+ window.open(url, "_blank");
+ },
+ }}
+ />
+ );
+};
+
+export default function BenchmarkResultCharts(): JSX.Element {
+ const [data, setData] = useState<{ entries: { Benchmark: [] } }>();
+
+ useEffect(() => {
+ if (!data) {
+ (async () => {
+ setData(
+ JSON.parse(
+ (await (await fetch(
+ "https://raw.githubusercontent.com/discordeno/discordeno/benchies/benchmarksResult/data.js",
+ )).text()).slice(24),
+ ),
+ );
+ })();
+ }
+ }, []);
+
+ function collectBenchesPerTestCase(entries) {
+ const dataMap = new Map();
+ for (const entry of entries) {
+ const { commit, date, tool, benches } = entry;
+ for (const bench of benches) {
+ const result = { commit, date, tool, bench };
+ const arr = dataMap.get(bench.name);
+ if (arr === undefined) {
+ dataMap.set(bench.name, [result]);
+ } else {
+ arr.push(result);
+ }
+ }
+ }
+ return dataMap;
+ }
+
+ return (
+
+ {data
+ ? Array.from(
+ collectBenchesPerTestCase(data.entries.Benchmark),
+ ([key, value]) => ({ benchName: key, benches: value }),
+ ).map((
+ bench,
+ index,
+ ) => (
+
+ ))
+ : <>>}
+
+ );
+}
diff --git a/apps/site/src/components/BenchmarkResultCharts.jsx b/apps/site/src/components/BenchmarkResultCharts.jsx
deleted file mode 100644
index ef040f82a..000000000
--- a/apps/site/src/components/BenchmarkResultCharts.jsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { useColorMode } from "@docusaurus/theme-common";
-import React, { useEffect, useState } from "react";
-import { Line } from "react-chartjs-2";
-// Colors from https://github.com/github/linguist/blob/master/lib/linguist/languages.yml
-const toolColors = {
- cargo: "#dea584",
- go: "#00add8",
- benchmarkjs: "#f1e05a",
- benchmarkluau: "#000080",
- pytest: "#3572a5",
- googlecpp: "#f34b7d",
- catch2: "#f34b7d",
- julia: "#a270ba",
- benchmarkdotnet: "#178600",
- customBiggerIsBetter: "#38ff38",
- customSmallerIsBetter: "#ff3838",
- _: "#333333",
-};
-
-const BenchmarkResultChart = ({ name, dataset, theme }) => {
- const color = toolColors[dataset.length > 0 ? dataset[0].tool : "_"];
- const data = {
- labels: dataset.map((d) => d.commit.id.slice(0, 7)),
- datasets: [
- {
- label: name,
- data: dataset.map((d) => d.bench.value),
- borderColor: color,
- backgroundColor: color + "60", // Add alpha for #rrggbbaa
- },
- ],
- };
- const options = {
- scales: {
- xAxes: [
- {
- scaleLabel: {
- display: true,
- labelString: "commit",
- },
- },
- ],
- yAxes: [
- {
- scaleLabel: {
- display: true,
- labelString: dataset.length > 0 ? dataset[0].bench.unit : "",
- },
- ticks: {
- beginAtZero: true,
- },
- },
- ],
- legend: {
- labels: {
- // This more specific font property overrides the global property
- defaultFontColor: theme === "light" ? "#000000" : "#FFFFFF",
- },
- },
- },
- tooltips: {
- callbacks: {
- afterTitle: (items) => {
- const { index } = items[0];
- const data = dataset[index];
- return "\n" + data.commit.message + "\n\n" + data.commit.timestamp + " committed by @" +
- data.commit.committer.username + "\n";
- },
- label: (item) => {
- let label = item.value;
- const { range, unit } = dataset[item.index].bench;
- label += " " + unit;
- if (range) {
- label += " (" + range + ")";
- }
- return label;
- },
- afterLabel: (item) => {
- const { extra } = dataset[item.index].bench;
- return extra ? "\n" + extra : "";
- },
- },
- },
- onClick: (_mouseEvent, activeElems) => {
- if (activeElems.length === 0) {
- return;
- }
- // XXX: Undocumented. How can we know the index?
- const index = activeElems[0]._index;
- const url = dataset[index].commit.url;
- window.open(url, "_blank");
- },
- };
- // console.log(theme);
- return ;
-};
-
-export default function BenchmarkResultCharts() {
- const { colorMode } = useColorMode();
- const [data, setData] = useState();
-
- useEffect(async () => {
- if (!data) {
- setData(
- JSON.parse(
- (await (await fetch(
- "https://raw.githubusercontent.com/discordeno/discordeno/benchies/benchmarksResult/data.js",
- )).text()).slice(24),
- ),
- );
- }
- }, []);
-
- function collectBenchesPerTestCase(entries) {
- const dataMap = new Map();
- for (const entry of entries) {
- const { commit, date, tool, benches } = entry;
- for (const bench of benches) {
- const result = { commit, date, tool, bench };
- const arr = dataMap.get(bench.name);
- if (arr === undefined) {
- dataMap.set(bench.name, [result]);
- } else {
- arr.push(result);
- }
- }
- }
- return dataMap;
- }
-
- return (data
- ? Array.from(
- collectBenchesPerTestCase(data.entries.Benchmark),
- ([key, value]) => ({ benchName: key, benches: value }),
- ).map((
- bench,
- index,
- ) => (
-
- ))
- : <>>);
-}
diff --git a/apps/site/src/components/architecture/BaseFlowChart.tsx b/apps/site/src/components/architecture/BaseFlowChart.tsx
new file mode 100644
index 000000000..bc1117bb2
--- /dev/null
+++ b/apps/site/src/components/architecture/BaseFlowChart.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useState } from "react";
+import ReactFlow, { Background, Controls, Edge, Handle, Node, Position, useEdgesState, useNodesState } from "reactflow";
+import "reactflow/dist/style.css";
+
+export const multiplier = 225;
+export const height = 40;
+export const widthMultiplier = 0.75;
+
+export const defaultNodeOptions = {
+ targetPosition: Position.Left,
+ sourcePosition: Position.Right,
+ draggable: false,
+ style: { width: `${multiplier * 0.75}px`, height: `${height}px` },
+};
+
+export const defaultGroupOptions = {
+ draggable: false,
+};
+
+export default function BaseFlowChart(
+ { initialNodes = [], initialEdges = [] }: { initialNodes: Node[]; initialEdges: Edge[] },
+) {
+ function getWindowDimensions() {
+ const { innerWidth: width, innerHeight: height } = window;
+ return {
+ width,
+ height,
+ };
+ }
+
+ const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+ useEffect(() => {
+ function handleResize() {
+ setWindowDimensions(getWindowDimensions());
+ }
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+
+ return (
+ <>
+ = 997
+ ? `${
+ (100 *
+ ((windowDimensions.width - 300 -
+ (windowDimensions.width >= 1620 ? (windowDimensions.width - 1620) * 0.5 : 0)) /
+ windowDimensions.width) - 2) * widthMultiplier
+ }vw`
+ : "95vw",
+ height: "25vh",
+ }}
+ >
+
(
+
+
+
+
+ ),
+ baseLineNodeText: (n) => (
+
+
{n.data.label}
+
+ ),
+ }}
+ fitView
+ >
+
+
+
+
+ >
+ );
+}
diff --git a/apps/site/src/components/architecture/FlowChart.tsx b/apps/site/src/components/architecture/FlowChart.tsx
new file mode 100644
index 000000000..d89ade8eb
--- /dev/null
+++ b/apps/site/src/components/architecture/FlowChart.tsx
@@ -0,0 +1,103 @@
+import React from "react";
+import { Edge, Node } from "reactflow";
+import "reactflow/dist/style.css";
+import BaseFlowChart, { defaultNodeOptions, multiplier } from "./BaseFlowChart";
+
+const initialNodes: Node[] = [
+ {
+ id: "discordGateway",
+ type: "input",
+ position: { x: 0 * multiplier, y: 0 },
+ data: { label: "Discord Gateway" },
+ ...defaultNodeOptions,
+ },
+ { id: "gateway", position: { x: 1 * multiplier, y: 0 }, data: { label: "Gateway" }, ...defaultNodeOptions },
+ { id: "baseLineNode-1", type: "baseLineNode", position: { x: 0.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-2", type: "baseLineNode", position: { x: 0.85 * multiplier, y: -130 }, data: {} },
+ { id: "baseLineNode-3", type: "baseLineNode", position: { x: 2.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-4", type: "baseLineNode", position: { x: 2.85 * multiplier, y: -130 }, data: {} },
+ { id: "baseLineNode-5", type: "baseLineNode", position: { x: 3.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-6", type: "baseLineNode", position: { x: 3.85 * multiplier, y: -130 }, data: {} },
+ { id: "baseLineNode-7", type: "baseLineNode", position: { x: 4.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-8", type: "baseLineNode", position: { x: 4.85 * multiplier, y: -130 }, data: {} },
+ {
+ id: "baseLineNodeText-1",
+ type: "baseLineNodeText",
+ position: { x: 0 * multiplier, y: -130 },
+ data: { label: "Discord" },
+ },
+ {
+ id: "baseLineNodeText-2",
+ type: "baseLineNodeText",
+ position: { x: 1.5 * multiplier, y: -130 },
+ data: { label: "Event In" },
+ },
+ {
+ id: "baseLineNodeText-3",
+ type: "baseLineNodeText",
+ position: { x: 3 * multiplier, y: -130 },
+ data: { label: "Event Processing" },
+ },
+ {
+ id: "baseLineNodeText-4",
+ type: "baseLineNodeText",
+ position: { x: 4 * multiplier, y: -130 },
+ data: { label: "Event out" },
+ },
+ {
+ id: "baseLineNodeText-6",
+ type: "baseLineNodeText",
+ position: { x: 5 * multiplier, y: -130 },
+ data: { label: "Discord" },
+ },
+ { id: "bot", position: { x: 2 * multiplier, y: 0 }, data: { label: "Bot" }, ...defaultNodeOptions },
+ { id: "yourCode", position: { x: 3 * multiplier, y: 0 }, data: { label: "Your Code" }, ...defaultNodeOptions },
+ { id: "rest", position: { x: 4 * multiplier, y: 0 }, data: { label: "Rest" }, ...defaultNodeOptions },
+ {
+ id: "discordApiGateway",
+ type: "output",
+ position: { x: 5 * multiplier, y: 0 },
+ data: { label: "Discord Api" },
+ ...defaultNodeOptions,
+ },
+];
+
+const initialEdges: Edge[] = [
+ { id: "d-g", source: "discordGateway", target: "gateway" },
+ { id: "g-b", source: "gateway", target: "bot" },
+ { id: "b-y", source: "bot", target: "yourCode" },
+ { id: "y-r", source: "yourCode", target: "rest" },
+ { id: "r-d", source: "rest", target: "discordApiGateway" },
+ {
+ id: "baseLine-1",
+ source: "baseLineNode-1",
+ target: "baseLineNode-2",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ {
+ id: "baseLine-2",
+ source: "baseLineNode-3",
+ target: "baseLineNode-4",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ {
+ id: "baseLine-3",
+ source: "baseLineNode-5",
+ target: "baseLineNode-6",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ {
+ id: "baseLine-4",
+ source: "baseLineNode-7",
+ target: "baseLineNode-8",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+];
+
+export default function FlowChart() {
+ return ;
+}
diff --git a/apps/site/src/components/architecture/FlowChart2.tsx b/apps/site/src/components/architecture/FlowChart2.tsx
new file mode 100644
index 000000000..6f10ee87d
--- /dev/null
+++ b/apps/site/src/components/architecture/FlowChart2.tsx
@@ -0,0 +1,198 @@
+import React from "react";
+import { Edge, Node, Position } from "reactflow";
+import "reactflow/dist/style.css";
+import BaseFlowChart, { defaultGroupOptions, defaultNodeOptions, multiplier } from "./BaseFlowChart";
+
+const initialNodes: Node[] = [
+ {
+ id: "discordGateway",
+ type: "input",
+ position: { x: 0 * multiplier, y: 0 },
+ data: { label: "Discord Gateway" },
+ ...defaultNodeOptions,
+ },
+ { id: "baseLineNode-1", type: "baseLineNode", position: { x: 0.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-2", type: "baseLineNode", position: { x: 0.85 * multiplier, y: -130 }, data: {} },
+ { id: "baseLineNode-3", type: "baseLineNode", position: { x: 3.75 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-4", type: "baseLineNode", position: { x: 3.75 * multiplier, y: -130 }, data: {} },
+ {
+ id: "baseLineNodeText-1",
+ type: "baseLineNodeText",
+ position: { x: 0 * multiplier, y: -130 },
+ data: { label: "Discord" },
+ },
+ {
+ id: "baseLineNodeText-2",
+ type: "baseLineNodeText",
+ position: { x: 2 * multiplier, y: -130 },
+ data: { label: "Gateway" },
+ },
+ {
+ id: "baseLineNodeText-3",
+ type: "baseLineNodeText",
+ position: { x: 4 * multiplier, y: -130 },
+ data: { label: "Bot" },
+ },
+ {
+ id: "gatewayManager",
+ type: "input",
+ position: { x: 2.0625 * multiplier, y: -80 },
+ data: { label: "Gateway Manager" },
+ ...defaultNodeOptions,
+ targetPosition: Position.Top,
+ sourcePosition: Position.Bottom,
+ },
+ {
+ id: "shard-1",
+ type: "output",
+ position: { x: 1.25 * multiplier, y: -20 },
+ data: { label: "Shard-1" },
+ style: { width: `${multiplier * 2}px`, height: "80px" },
+ ...defaultGroupOptions,
+ },
+ {
+ id: "shard-1-socket",
+ position: { x: 0.125 * multiplier, y: 20 },
+ data: { label: "webSocket" },
+ ...defaultNodeOptions,
+ parentNode: "shard-1",
+ extent: "parent",
+ },
+ {
+ id: "shard-1-handleMessage",
+ position: { x: 1.125 * multiplier, y: 20 },
+ data: { label: "HandleMessage" },
+ ...defaultNodeOptions,
+ parentNode: "shard-1",
+ extent: "parent",
+ },
+ {
+ id: "shard-2",
+ type: "output",
+ position: { x: 1.375 * multiplier, y: 0 },
+ data: { label: "Shard-2" },
+ style: { width: `${multiplier * 2}px`, height: "80px" },
+ ...defaultGroupOptions,
+ },
+ {
+ id: "shard-2-socket",
+ position: { x: 0.125 * multiplier, y: 20 },
+ data: { label: "webSocket" },
+ ...defaultNodeOptions,
+ parentNode: "shard-2",
+ extent: "parent",
+ },
+ {
+ id: "shard-2-handleMessage",
+ position: { x: 1.125 * multiplier, y: 20 },
+ data: { label: "HandleMessage" },
+ ...defaultNodeOptions,
+ parentNode: "shard-2",
+ extent: "parent",
+ },
+ {
+ id: "shard-3",
+ type: "output",
+ position: { x: 1.5 * multiplier, y: 20 },
+ data: { label: "Shard-3" },
+ style: { width: `${multiplier * 2}px`, height: "80px" },
+ ...defaultGroupOptions,
+ },
+ {
+ id: "shard-3-socket",
+ position: { x: 0.125 * multiplier, y: 20 },
+ data: { label: "webSocket" },
+ ...defaultNodeOptions,
+ parentNode: "shard-3",
+ extent: "parent",
+ },
+ {
+ id: "shard-3-handleMessage",
+ position: { x: 1.125 * multiplier, y: 20 },
+ data: { label: "HandleMessage" },
+ ...defaultNodeOptions,
+ parentNode: "shard-3",
+ extent: "parent",
+ },
+ {
+ id: "shard-n",
+ type: "output",
+ position: { x: 1.625 * multiplier, y: 40 },
+ data: { label: "Shard-N" },
+ style: { width: `${multiplier * 2}px`, height: "80px" },
+ ...defaultGroupOptions,
+ },
+ {
+ id: "shard-n-socket",
+ position: { x: 0.125 * multiplier, y: 20 },
+ data: { label: "webSocket" },
+ ...defaultNodeOptions,
+ parentNode: "shard-n",
+ extent: "parent",
+ },
+ {
+ id: "shard-n-handleMessage",
+ position: { x: 1.125 * multiplier, y: 20 },
+ data: { label: "HandleMessage" },
+ ...defaultNodeOptions,
+ parentNode: "shard-n",
+ extent: "parent",
+ },
+ { id: "bot", type: "output", position: { x: 4 * multiplier, y: 0 }, data: { label: "Bot" }, ...defaultNodeOptions },
+];
+
+const initialEdges: Edge[] = [
+ { id: "d-g", source: "discordGateway", target: "gateway" },
+ { id: "g-b", source: "gateway", target: "bot" },
+ { id: "b-y", source: "bot", target: "yourCode" },
+ { id: "y-r", source: "yourCode", target: "rest" },
+ { id: "r-d", source: "rest", target: "discordApiGateway" },
+ {
+ id: "baseLine-1",
+ source: "baseLineNode-1",
+ target: "baseLineNode-2",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ {
+ id: "baseLine-2",
+ source: "baseLineNode-3",
+ target: "baseLineNode-4",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ {
+ id: "baseLine-3",
+ source: "baseLineNode-5",
+ target: "baseLineNode-6",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ {
+ id: "baseLine-4",
+ source: "baseLineNode-7",
+ target: "baseLineNode-8",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ animated: false,
+ },
+ { id: "d-shard-1", source: "discordGateway", target: "shard-1-socket", zIndex: 100 },
+ { id: "d-shard-2", source: "discordGateway", target: "shard-2-socket", zIndex: 100 },
+ { id: "d-shard-3", source: "discordGateway", target: "shard-3-socket", zIndex: 100 },
+ { id: "d-shard-n", source: "discordGateway", target: "shard-n-socket", zIndex: 100 },
+ { id: "shard-1-socket-handleMessage", source: "shard-1-socket", target: "shard-1-handleMessage", zIndex: 100 },
+ { id: "shard-2-socket-handleMessage", source: "shard-2-socket", target: "shard-2-handleMessage", zIndex: 100 },
+ { id: "shard-3-socket-handleMessage", source: "shard-3-socket", target: "shard-3-handleMessage", zIndex: 100 },
+ { id: "shard-n-socket-handleMessage", source: "shard-n-socket", target: "shard-n-handleMessage", zIndex: 100 },
+ { id: "shard-1-handleMessage-bot", source: "shard-1-handleMessage", target: "bot", zIndex: 100 },
+ { id: "shard-2-handleMessage-bot", source: "shard-2-handleMessage", target: "bot", zIndex: 100 },
+ { id: "shard-3-handleMessage-bot", source: "shard-3-handleMessage", target: "bot", zIndex: 100 },
+ { id: "shard-n-handleMessage-bot", source: "shard-n-handleMessage", target: "bot", zIndex: 100 },
+ { id: "gatewayManager-shard-1", source: "gatewayManager", target: "shard-1", zIndex: 10 },
+ { id: "gatewayManager-shard-2", source: "gatewayManager", target: "shard-2", zIndex: 100 },
+ { id: "gatewayManager-shard-3", source: "gatewayManager", target: "shard-3", zIndex: 100 },
+ { id: "gatewayManager-shard-n", source: "gatewayManager", target: "shard-n", zIndex: 100 },
+];
+
+export default function FlowChart2() {
+ return ;
+}
diff --git a/apps/site/src/components/architecture/FlowChart3.tsx b/apps/site/src/components/architecture/FlowChart3.tsx
new file mode 100644
index 000000000..6131215e4
--- /dev/null
+++ b/apps/site/src/components/architecture/FlowChart3.tsx
@@ -0,0 +1,669 @@
+import React, { useEffect, useState } from "react";
+import ReactFlow, {
+ Background,
+ Controls,
+ Edge,
+ Handle,
+ Node,
+ NodeMouseHandler,
+ Position,
+ useEdgesState,
+ useNodesState,
+} from "reactflow";
+import "reactflow/dist/style.css";
+import { defaultNodeOptions, height, multiplier, widthMultiplier } from "./BaseFlowChart";
+
+const handlers: {
+ [index: string]: {
+ transformers: string[];
+ event: string;
+ };
+} = {
+ handleChannelCreate: { transformers: ["transformers.channel"], event: "events.channelCreate" },
+ handleChannelDelete: {
+ transformers: ["transformers.channel", "transformers.snowflake"],
+ event: "events.channelDelete",
+ },
+ handleChannelPinsUpdate: {
+ transformers: ["transformers.snowflake", "transformers.snowflake"],
+ event: "events.channelPinsUpdate",
+ },
+ handleChannelUpdate: { transformers: ["transformers.channel"], event: "events.channelUpdate" },
+ handleStageInstanceCreate: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.stageInstanceCreate",
+ },
+ handleStageInstanceDelete: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.stageInstanceDelete",
+ },
+ handleStageInstanceUpdate: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.stageInstanceUpdate",
+ },
+ handleThreadCreate: { transformers: ["transformers.channel"], event: "events.threadCreate" },
+ handleThreadDelete: { transformers: ["transformers.channel"], event: "events.threadDelete" },
+ handleThreadListSync: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.channel",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ ],
+ event: undefined,
+ },
+ handleThreadMembersUpdate: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.threadMember",
+ "transformers.snowflake",
+ ],
+ event: "events.threadMembersUpdate",
+ },
+ handleThreadMemberUpdate: {
+ transformers: ["transformers.snowflake", "transformers.snowflake"],
+ event: "events.threadMemberUpdate",
+ },
+ handleThreadUpdate: { transformers: ["transformers.channel"], event: "events.threadUpdate" },
+ handleGuildEmojisUpdate: {
+ transformers: ["transformers.snowflake", "transformers.snowflake"],
+ event: "events.guildEmojisUpdate",
+ },
+ handleAutoModerationActionExecution: {
+ transformers: ["transformers.automodActionExecution"],
+ event: "events.automodActionExecution",
+ },
+ handleAutoModerationRuleCreate: { transformers: ["transformers.automodRule"], event: "events.automodRuleCreate" },
+ handleAutoModerationRuleDelete: { transformers: ["transformers.automodRule"], event: "events.automodRuleDelete" },
+ handleAutoModerationRuleUpdate: { transformers: ["transformers.automodRule"], event: "events.automodRuleUpdate" },
+ handleGuildBanAdd: {
+ transformers: ["transformers.user", "transformers.snowflake"],
+ event: "events.guildBanAdd",
+ },
+ handleGuildBanRemove: {
+ transformers: ["transformers.user", "transformers.snowflake"],
+ event: "events.guildBanRemove",
+ },
+ handleGuildCreate: { transformers: ["transformers.guild"], event: "events.guildCreate" },
+ handleGuildDelete: { transformers: ["transformers.snowflake"], event: "events.guildDelete" },
+ handleGuildIntegrationsUpdate: { transformers: ["transformers.snowflake"], event: "events.integrationUpdate" },
+ handleGuildUpdate: { transformers: ["transformers.guild"], event: "events.guildUpdate" },
+ handleGuildScheduledEventCreate: {
+ transformers: ["transformers.scheduledEvent"],
+ event: "events.scheduledEventCreate",
+ },
+ handleGuildScheduledEventDelete: {
+ transformers: ["transformers.scheduledEvent"],
+ event: "events.scheduledEventDelete",
+ },
+ handleGuildScheduledEventUpdate: {
+ transformers: ["transformers.scheduledEvent"],
+ event: "events.scheduledEventUpdate",
+ },
+ handleGuildScheduledEventUserAdd: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.scheduledEventUserAdd",
+ },
+ handleGuildScheduledEventUserRemove: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.scheduledEventUserRemove",
+ },
+ handleIntegrationCreate: { transformers: ["transformers.integration"], event: "events.integrationCreate" },
+ handleIntegrationDelete: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.integrationDelete",
+ },
+ handleIntegrationUpdate: { transformers: ["transformers.integration"], event: "events.integrationUpdate" },
+ handleInteractionCreate: {
+ transformers: ["transformers.snowflake", "transformers.interaction"],
+ event: "events.interactionCreate",
+ },
+ handleInviteCreate: { transformers: ["transformers.invite"], event: "events.inviteCreate" },
+ handleInviteDelete: {
+ transformers: ["transformers.snowflake", "transformers.snowflake"],
+ event: "events.inviteDelete",
+ },
+ handleGuildMembersChunk: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.member",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.user",
+ "transformers.activity",
+ ],
+ event: undefined,
+ },
+ handleGuildMemberAdd: {
+ transformers: ["transformers.snowflake", "transformers.user", "transformers.member"],
+ event: "events.guildMemberAdd",
+ },
+ handleGuildMemberRemove: {
+ transformers: ["transformers.snowflake", "transformers.user"],
+ event: "events.guildMemberRemove",
+ },
+ handleGuildMemberUpdate: {
+ transformers: ["transformers.user", "transformers.member", "transformers.snowflake"],
+ event: "events.guildMemberUpdate",
+ },
+ handleMessageCreate: { transformers: ["transformers.message"], event: "events.messageCreate" },
+ handleMessageDelete: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.messageDelete",
+ },
+ handleMessageDeleteBulk: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ ],
+ event: "events.messageDeleteBulk",
+ },
+ handleMessageReactionAdd: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.member",
+ "transformers.user",
+ "transformers.emoji",
+ ],
+ event: "events.reactionAdd",
+ },
+ handleMessageReactionRemove: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.emoji",
+ ],
+ event: "events.reactionRemove",
+ },
+ handleMessageReactionRemoveAll: {
+ transformers: ["transformers.snowflake", "transformers.snowflake", "transformers.snowflake"],
+ event: "events.reactionRemoveAll",
+ },
+ handleMessageReactionRemoveEmoji: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.emoji",
+ ],
+ event: "events.reactionRemoveEmoji",
+ },
+ handleMessageUpdate: { transformers: ["transformers.message"], event: "events.messageUpdate" },
+ handlePresenceUpdate: { transformers: ["transformers.presence"], event: "events.presenceUpdate" },
+ handleReady: {
+ transformers: [
+ "transformers.user",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ ],
+ event: "events.ready",
+ },
+ handleTypingStart: {
+ transformers: [
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.snowflake",
+ "transformers.member",
+ ],
+ event: "events.typingStart",
+ },
+ handleUserUpdate: { transformers: ["transformers.user"], event: "events.botUpdate" },
+ handleGuildRoleCreate: {
+ transformers: ["transformers.role", "transformers.snowflake"],
+ event: "events.roleCreate",
+ },
+ handleGuildRoleDelete: {
+ transformers: ["transformers.snowflake", "transformers.snowflake"],
+ event: "events.roleDelete",
+ },
+ handleGuildRoleUpdate: {
+ transformers: ["transformers.role", "transformers.snowflake"],
+ event: "events.roleUpdate",
+ },
+ handleVoiceServerUpdate: { transformers: ["transformers.snowflake"], event: "events.voiceServerUpdate" },
+ handleVoiceStateUpdate: {
+ transformers: ["transformers.snowflake", "transformers.voiceState"],
+ event: "events.voiceStateUpdate",
+ },
+ handleWebhooksUpdate: {
+ transformers: ["transformers.snowflake", "transformers.snowflake"],
+ event: "events.webhooksUpdate",
+ },
+};
+
+export default function FlowChart({ handlerFilter = (handler: string) => true }) {
+ function getWindowDimensions() {
+ const { innerWidth: width, innerHeight: height } = window;
+ return {
+ width,
+ height,
+ };
+ }
+
+ const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+ useEffect(() => {
+ function handleResize() {
+ setWindowDimensions(getWindowDimensions());
+ }
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ const transformers = [];
+
+ const events = [];
+
+ const initialNodes: Node[] = [
+ {
+ id: "baseNode-gateway",
+ type: "input",
+ position: { x: 0 * multiplier, y: 0 },
+ data: { label: "gateway" },
+ ...defaultNodeOptions,
+ },
+ {
+ id: "baseNode-yourCode",
+ type: "output",
+ position: { x: 5 * multiplier, y: 0 },
+ data: { label: "Your Code" },
+ ...defaultNodeOptions,
+ },
+ { id: "baseLineNode-1", type: "baseLineNode", position: { x: 0.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-2", type: "baseLineNode", position: { x: 0.85 * multiplier, y: -130 }, data: {} },
+ { id: "baseLineNode-3", type: "baseLineNode", position: { x: 4.85 * multiplier, y: 170 }, data: {} },
+ { id: "baseLineNode-4", type: "baseLineNode", position: { x: 4.85 * multiplier, y: -130 }, data: {} },
+ {
+ id: "baseLineNodeText-1",
+ type: "baseLineNodeText",
+ position: { x: 0 * multiplier, y: -130 },
+ data: { label: "Gateway" },
+ },
+ {
+ id: "baseLineNodeText-2",
+ type: "baseLineNodeText",
+ position: { x: 1 * multiplier, y: -130 },
+ data: { label: "Bot" },
+ },
+ {
+ id: "baseLineNodeText-3",
+ type: "baseLineNodeText",
+ position: { x: 5 * multiplier, y: -130 },
+ data: { label: "Your Code" },
+ },
+ {
+ id: "baseNode-handleDiscordPayload",
+ position: { x: 1 * multiplier, y: 0 },
+ data: { label: "Handle discord payload" },
+ ...defaultNodeOptions,
+ },
+ ];
+
+ const initialEdges: Edge[] = [
+ { id: "baseEdge-1", source: "baseNode-gateway", target: "baseNode-handleDiscordPayload" },
+ { id: "baseEdge-3", source: "baseNode-g-1", target: "baseNode-g-2" },
+ { id: "baseEdge-4", source: "baseNode-g-2", target: "baseNode-handleDiscordPayload" },
+ {
+ id: "baseLine-1",
+ source: "baseLineNode-1",
+ target: "baseLineNode-2",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ },
+ {
+ id: "baseLine-2",
+ source: "baseLineNode-3",
+ target: "baseLineNode-4",
+ style: { stroke: "blue", strokeDasharray: 20 },
+ },
+ ];
+
+ //@ts-ignore
+ for (const [index, handler] of Object.keys(handlers).filter(handlerFilter).entries()) {
+ initialNodes.push({
+ id: handler,
+ position: {
+ x: 2 * multiplier,
+ y: index * (height + 10) - Object.keys(handlers).filter(handlerFilter).length * ((height + 10) / 2) +
+ height / 2,
+ },
+ data: { label: handler.slice(6) },
+ ...defaultNodeOptions,
+ });
+ initialEdges.push({
+ id: `handleDiscordPayload-${handler}`,
+ source: "baseNode-handleDiscordPayload",
+ target: handler,
+ });
+ if (!events.find((e) => e === handlers[handler].event) && handlers[handler].event) {
+ events.push(handlers[handler].event);
+ initialEdges.push({
+ id: `${handlers[handler].event}-yourCode`,
+ source: handlers[handler].event,
+ target: "baseNode-yourCode",
+ });
+ }
+ for (const transformer of handlers[handler].transformers) {
+ if (!transformers.find((t) => t === transformer) && transformer) transformers.push(transformer);
+ if (!initialEdges.find((edge) => edge.id === `${handler}-${transformer}`) && transformer) {
+ initialEdges.push({ id: `${handler}-${transformer}`, source: handler, target: transformer });
+ }
+ if (
+ !initialEdges.find((edge) => edge.id === `${transformer}-${handlers[handler].event}`) && handlers[handler].event
+ ) {
+ initialEdges.push({
+ id: `${transformer}-${handlers[handler].event}`,
+ source: transformer,
+ target: handlers[handler].event,
+ });
+ }
+ }
+ }
+
+ //@ts-ignore
+ for (const [index, transformer] of transformers.entries()) {
+ initialNodes.push({
+ id: transformer,
+ position: {
+ x: 3 * multiplier,
+ y: index * (height + 10) - transformers.length * ((height + 10) / 2) + height / 2,
+ },
+ data: { label: transformer.slice(13) },
+ ...defaultNodeOptions,
+ });
+ }
+
+ //@ts-ignore
+ for (const [index, event] of events.entries()) {
+ initialNodes.push({
+ id: event,
+ position: { x: 4 * multiplier, y: index * (height + 10) - events.length * ((height + 10) / 2) + height / 2 },
+ data: { label: event.slice(7) },
+ ...defaultNodeOptions,
+ });
+ }
+
+ initialNodes.unshift(
+ {
+ id: "handlers",
+ type: "group",
+ position: {
+ x: 1.925 * multiplier,
+ y: -Object.keys(handlers).filter(handlerFilter).length * ((height + 10) / 2) - 45,
+ },
+ data: { label: "" },
+ style: {
+ height: `${Object.keys(handlers).filter(handlerFilter).length * (height + 10) + 75}px`,
+ width: `${multiplier * 0.9}px`,
+ borderColor: "rgba(0,0,0,0.25)",
+ },
+ draggable: false,
+ },
+ {
+ id: "baseLineNodeText-4",
+ type: "baseLineNodeText",
+ position: {
+ x: 2 * multiplier,
+ y: -Object.keys(handlers).filter(handlerFilter).length * ((height + 10) / 2) - ((height + 10) / 2),
+ },
+ data: { label: "Handlers" },
+ draggable: false,
+ },
+ {
+ id: "transformers",
+ type: "group",
+ position: { x: 2.925 * multiplier, y: -transformers.length * ((height + 10) / 2) - 45 },
+ data: { label: "" },
+ style: {
+ height: `${transformers.length * (height + 10) + 75}px`,
+ width: `${multiplier * 0.9}px`,
+ borderColor: "rgba(0,0,0,0.25)",
+ },
+ draggable: false,
+ },
+ {
+ id: "baseLineNodeText-5",
+ type: "baseLineNodeText",
+ position: { x: 3 * multiplier, y: -transformers.length * ((height + 10) / 2) - ((height + 10) / 2) },
+ data: { label: "Transformers" },
+ draggable: false,
+ },
+ {
+ id: "events",
+ type: "group",
+ position: { x: 3.925 * multiplier, y: -events.length * ((height + 10) / 2) - 45 },
+ data: { label: "" },
+ style: {
+ height: `${events.length * (height + 10) + 75}px`,
+ width: `${multiplier * 0.9}px`,
+ borderColor: "rgba(0,0,0,0.25)",
+ },
+ draggable: false,
+ },
+ {
+ id: "baseLineNodeText-6",
+ type: "baseLineNodeText",
+ position: { x: 4 * multiplier, y: -events.length * ((height + 10) / 2) - ((height + 10) / 2) },
+ data: { label: "Event" },
+ draggable: false,
+ },
+ );
+
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+ const [handlerIndex, setHandlerIndex] = useState(0);
+ const [userClick, setUserClick] = useState(false);
+
+ const nodeMouseHandler: NodeMouseHandler = (_, node, userTrigger = true) => {
+ if (userTrigger) setUserClick(true);
+ if (node.id.split("-")[0] === "baseNode") {
+ edges.forEach((e) => {
+ if (e.id.startsWith("baseLine")) return;
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ });
+ setEdges([...edges]);
+ return;
+ }
+ if (Object.keys(handlers).find((h) => handlers[h].event === node.id)) {
+ const handlerName = Object.keys(handlers).find((h) => handlers[h].event === node.id);
+ const handler = handlers[handlerName];
+ edges.forEach((e) => {
+ if (e.id.startsWith("baseLine")) return;
+ if (e.id.split("-")[0] === "baseEdge") {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === "handleDiscordPayload" && e.id.split("-")[1] === handlerName) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === handlerName && handler.transformers.includes(e.id.split("-")[1])) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (handler.transformers.includes(e.id.split("-")[0]) && e.id.split("-")[1] === handler.event) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === handler.event && e.id.split("-")[1] === "yourCode") {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ e.animated = false;
+ e.style = { opacity: 0.3 };
+ });
+ setEdges([...edges]);
+ return;
+ }
+ if (Object.keys(handlers).find((h) => handlers[h].transformers.includes(node.id))) {
+ edges.forEach((e) => {
+ if (e.id.startsWith("baseLine")) return;
+ if (e.id.split("-")[0] === "baseEdge") {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (
+ e.id.split("-")[0] === "handleDiscordPayload" &&
+ Object.keys(handlers).filter((h) => handlers[h].transformers.includes(node.id)).includes(e.id.split("-")[1])
+ ) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (
+ Object.keys(handlers).filter((h) => handlers[h].transformers.includes(node.id)).includes(
+ e.id.split("-")[0],
+ ) && e.id.split("-")[1] === node.id
+ ) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === node.id) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ e.animated = false;
+ e.style = { opacity: 0.3 };
+ });
+ setEdges([...edges]);
+ return;
+ }
+ if (handlers[node.id]) {
+ const handler = handlers[node.id];
+ edges.forEach((e) => {
+ if (e.id.startsWith("baseLine")) return;
+ if (e.id.split("-")[0] === "baseEdge") {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === "handleDiscordPayload" && e.id.split("-")[1] === node.id) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === node.id && handler.transformers.includes(e.id.split("-")[1])) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (handler.transformers.includes(e.id.split("-")[0]) && e.id.split("-")[1] === handler.event) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ if (e.id.split("-")[0] === handler.event) {
+ e.animated = true;
+ e.style = { stroke: "blue" };
+ return;
+ }
+ e.animated = false;
+ e.style = { opacity: 0.3 };
+ });
+ setEdges([...edges]);
+ return;
+ }
+ edges.forEach((e) => {
+ if (e.id.startsWith("baseLine")) return;
+ e.animated = false;
+ e.style = {};
+ });
+ setEdges([...edges]);
+ };
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const randomIndex = Math.round((Object.keys(handlers).filter(handlerFilter).length - 1) * Math.random());
+ if (!userClick) {
+ nodeMouseHandler(undefined, { id: Object.keys(handlers).filter(handlerFilter)[randomIndex] }, false);
+ }
+ setHandlerIndex(randomIndex);
+ }, 1000);
+ return () => clearInterval(interval);
+ }, [userClick]);
+
+ useEffect(() => {
+ if (userClick) {
+ const timeout = setTimeout(() => {
+ setUserClick(false);
+ }, 10000);
+ return () => clearTimeout(timeout);
+ }
+ }, [userClick]);
+
+ return (
+ <>
+ = 997
+ ? `${
+ (100 *
+ ((windowDimensions.width - 300 -
+ (windowDimensions.width >= 1620 ? (windowDimensions.width - 1620) * 0.5 : 0)) /
+ windowDimensions.width) - 2) * widthMultiplier
+ }vw`
+ : "95vw",
+ height: "50vh",
+ }}
+ >
+
{
+ //@ts-ignore
+ if (e.target.className === "react-flow__pane") nodeMouseHandler(e, { id: " - ", data: { label: " - " } });
+ }}
+ nodeTypes={{
+ baseLineNode: () => (
+
+
+
+
+ ),
+ baseLineNodeText: (n) => (
+
+
{n.data.label}
+
+ ),
+ }}
+ fitView
+ >
+
+
+
+
+ >
+ );
+}
diff --git a/apps/site/src/pages/index.tsx b/apps/site/src/pages/index.tsx
index 0fa82d657..21d3bc09e 100644
--- a/apps/site/src/pages/index.tsx
+++ b/apps/site/src/pages/index.tsx
@@ -1,10 +1,10 @@
-import React from "react";
-import clsx from "clsx";
-import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
-import styles from "./index.module.css";
+import Layout from "@theme/Layout";
+import clsx from "clsx";
+import React from "react";
import HomepageFeatures from "../components/HomepageFeatures";
+import styles from "./index.module.css";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
diff --git a/apps/site/tutorial/amethyst/AmethystCollection.md b/apps/site/tutorial/amethyst/AmethystCollection.md
new file mode 100644
index 000000000..14e0ddf68
--- /dev/null
+++ b/apps/site/tutorial/amethyst/AmethystCollection.md
@@ -0,0 +1,7 @@
+---
+sidebar_position: 5
+---
+
+# Same as discord.js [collection][def]
+
+[def]: https://discord.js.org/#/docs/collection/main/class/Collection
diff --git a/apps/site/tutorial/amethyst/AmethystEmbed.md b/apps/site/tutorial/amethyst/AmethystEmbed.md
new file mode 100644
index 000000000..f654b38d6
--- /dev/null
+++ b/apps/site/tutorial/amethyst/AmethystEmbed.md
@@ -0,0 +1,7 @@
+---
+sidebar_position: 4
+---
+
+# Documentation [at][def]
+
+[def]: https://deno.land/x/amethyst@v4.3.4/mod.ts?s=AmethystEmbed
diff --git a/apps/site/tutorial/amethyst/_category_.json b/apps/site/tutorial/amethyst/_category_.json
new file mode 100644
index 000000000..b564ff089
--- /dev/null
+++ b/apps/site/tutorial/amethyst/_category_.json
@@ -0,0 +1 @@
+{ "label": "Amethyst Framework", "position": 4 }
diff --git a/apps/site/tutorial/amethyst/client.md b/apps/site/tutorial/amethyst/client.md
new file mode 100644
index 000000000..cc2fba13d
--- /dev/null
+++ b/apps/site/tutorial/amethyst/client.md
@@ -0,0 +1,99 @@
+---
+sidebar_position: 2
+---
+
+# Creating an client
+
+Let's review each choice and what it does.
+
+- `owners`, You may specify the proprietors of the bot using this. The inhibitors make use of this.
+- `prefix`, The string a user should use at the beginning of their message to identify it as a command to the bot. Only
+ message commands can use this, and the parameter can be either a string or a function.
+- `botMentionAsPrefix`, Determines whether a user's mention of a bot qualifies as a prefix.
+- `ignoreBots`, Allow bots to execute commands.
+- `defaultCooldown`, Defualt cooldown for all commands.
+- `ignoreCooldown`, List of people who bypass cooldowns.
+- `commandDir`, Path to the command directory used by the fileloader.
+- `eventDir`, Path to the event directory used by the fileloader.
+- `inhibitorDir`, Path to the inhibitor directory used by the fileloader.
+- `prefixCaseSensitive`, Indicates whether or not the prefix is case-sensitive.
+- `extras`, Extras that are used by your client, such as a database instance or a music player.
+
+## Client Extras
+
+When using discord.js we often do stuff like `client.musicplayer=player;` and in order to maintain this ease Amethyst
+allows you to do `client.extras.musicplayer=player;`.
+
+NOTE: Typing will not work on `client.extras`.
+
+## Client Properties
+
+```ts
+ user: User;
+ events: AmethystEvents;
+ messageCollectors: AmethystCollection;
+ componentCollectors: AmethystCollection;
+ reactionCollectors: AmethystCollection;
+ runningTasks: runningTasks;
+ tasks: AmethystCollection;
+ category: AmethystCollection;
+ inhibitors: AmethystCollection<
+ string,
+ (
+ bot: AmethystBot,
+ command: T,
+ options: { memberId?: bigint; channelId: bigint; guildId?: bigint }
+ ) => true | AmethystError
+ >;
+ owners?: bigint[];
+ botMentionAsPrefix?: boolean;
+ prefixCaseSensitive?: boolean;
+ defaultCooldown?: CommandCooldown;
+ ignoreCooldown?: bigint[];
+ guildOnly?: boolean;
+ messageQuotedArguments?: boolean;
+ ignoreBots?: boolean;
+ dmOnly?: boolean;
+ eventHandler: AmethystEventHandler;
+ extras: any;
+ prefix?:
+ | string
+ | string[]
+ | ((bot: AmethystBot, message: Message) => Async);
+
+ on(name: string, callback: (...args: any) => unknown): void;
+ once(name: string, callback: (...args: any) => unknown): void;
+
+ amethystUtils: {
+ awaitComponent(
+ messageId: bigint,
+ options?: ComponentCollectorOptions & { maxUsage?: number }
+ ): Promise,
+ awaitReaction(
+ messageId: bigint,
+ options?: ReactionCollectorOptions & { maxUsage?: number }
+ ): Promise,
+ awaitMessage(
+ memberId: bigint,
+ channelId: bigint,
+ options?: MessageCollectorOptions & { maxUsage?: number }
+ ): Promise,
+ createCommand(command: CommandOptions): void,
+ createCategory(category: CategoryOptions): void,
+ updateCategory(category: CategoryOptions): void,
+ createTask(task: AmethystTask): void,
+ clearTasks(): void,
+ createInhibitor(
+ name: string,
+ inhibitor: (
+ bot: AmethystBot,
+ command: T,
+ options?: { memberId?: bigint; guildId?: bigint; channelId: bigint }
+ ) => true | AmethystError
+ ): void,
+ deleteInhibitor(name: string): void,
+ updateSlashCommands(): void,
+ }
+```
+
+## [Documentation](https://deno.land/x/amethyst@v4.2.0/mod.ts?s=AmethystBotOptions)
diff --git a/apps/site/tutorial/amethyst/exampleBot.md b/apps/site/tutorial/amethyst/exampleBot.md
new file mode 100644
index 000000000..8dd89659e
--- /dev/null
+++ b/apps/site/tutorial/amethyst/exampleBot.md
@@ -0,0 +1,79 @@
+---
+sidebar_position: 3
+---
+
+# Lets Create a simple bot in Node.js
+
+- **Step 1**: Create a typescript project with index.ts as main file.
+
+- **Step 2**: Installing packages. Install following packages.
+
+```bash
+npm i @thereallonewolf/amethystframework
+```
+
+- **Step 3**: Create a index.ts file.
+
+- **Step 4**: Add following code in index.ts file, replacing TOKEN with your bot token.
+
+```ts
+import { createBot, GatewayIntents, startBot } from "discordeno";
+import { enableCachePlugin, enableCacheSweepers } from "discordeno/cache-plugin";
+import {
+ AmethystBot,
+ Category,
+ Command,
+ Context,
+ enableAmethystPlugin,
+ Event,
+} from "@thereallonewolf/amethystframework";
+
+let baseClient = createBot({
+ token: "TOKEN",
+ intents: GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.MessageContent,
+});
+
+//@ts-ignore
+let client = enableAmethystPlugin(enableCachePlugin(baseClient), {
+ botMentionAsPrefix: true,
+ prefix: "!", //Can be a function or a string.
+ ignoreBots: false,
+});
+enableCacheSweepers(client);
+
+startBot(client);
+
+@Category({
+ name: "general",
+ description: "My general commands",
+ uniqueCommands: true,
+ default: "", //As all the commands are unique so no need to set the default command.
+})
+export class General {
+ @Command({
+ name: "ping",
+ description: "Pong!",
+ commandType: ["application", "message"],
+ category: "general",
+ args: [],
+ })
+ async ping(bot: AmethystBot, ctx: Context) {
+ ctx.reply({ content: "Pong!" });
+ }
+
+ @Event("ready")
+ async ready() {
+ console.log("I am ready!");
+ client.amethystUtils.updateSlashCommands();
+ }
+}
+```
+
+- **Step 5**: Invite your bot and compile index.ts and run it. Then you can use `/general ping` or `!ping`
+
+- **Step 6**: Useful links:
+
+1. Command Options can be found
+ [here](https://github.com/AmethystFramework/framework/blob/master/src/types/commandOptions.ts).
+2. Category Options [here](https://github.com/AmethystFramework/framework/blob/master/src/types/categoryOptions.ts)
+3. Full [Documentation](https://deno.land/x/amethyst)
diff --git a/apps/site/tutorial/amethyst/intro.md b/apps/site/tutorial/amethyst/intro.md
new file mode 100644
index 000000000..174d5e428
--- /dev/null
+++ b/apps/site/tutorial/amethyst/intro.md
@@ -0,0 +1,54 @@
+---
+sidebar_position: 1
+---
+
+# Amethyst
+
+Amethyst is a [Discordeno](https://github.com/discordeno/discordeno) plugin that is incredibly robust and flexible. It
+promotes standard practices and is geared at bigger bots.
+
+This framework is not for you if you cannot utilise Maps and Sets without reading them up. We presume you have a solid
+foundation in typescript/javascript and dicord bots.
+
+[Documentation](https://deno.land/x/amethyst)
+
+## Features
+
+- Thanks to Amethyst's adaptability, you can change a lot of things and add features as you see appropriate.
+- A developer may create slash or message interactions with Amethyst.
+- Assistance with interactions, such as selection, built-in buttons, and more.
+- Custom discord.js like event system without the event emitter.
+- Explore more incredible features of our framework.
+
+## Why Amethyst?
+
+Amethyst makes use of the [Discordeno](https://github.com/discordeno/discordeno) plugin system to streamline your coding
+process and help you get going more quickly. Support for message and slash commands that doesn't need altering any code.
+
+- Fully programmable
+
+- Easy to learn and utilise.
+
+- Conversion from Discord.js bots is simple.
+
+## Ideas
+
+The objective is to provide a framework that can be used with both Node.js and Deno while minimising transitions and
+rewrites.
+
+- Easy to understand and use.
+- Cover up complexity in the engine keeping the end user interface as simple as possible.
+
+## Future Updates
+
+Creation of discord setup wizards for commands like welcome, context menus and paginated messages.
+
+## Installation
+
+Deno: [link](https://deno.land/x/amethyst)
+
+Npm:
+
+```bash
+npm i @thereallonewolf/amethystframework
+```
diff --git a/apps/site/tutorial/big-bot-guide/_category_.json b/apps/site/tutorial/big-bot-guide/_category_.json
new file mode 100644
index 000000000..3ff52b832
--- /dev/null
+++ b/apps/site/tutorial/big-bot-guide/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Big Bot Guide",
+ "position": 2
+}
diff --git a/apps/site/tutorial/big-bot-guide/cache.md b/apps/site/tutorial/big-bot-guide/cache.md
new file mode 100644
index 000000000..da148767a
--- /dev/null
+++ b/apps/site/tutorial/big-bot-guide/cache.md
@@ -0,0 +1,119 @@
+---
+sidebar_position: 4
+sidebar_label: Step 3 - Cache
+---
+
+# Step 3: Standalone Cache Process
+
+The next part of this is going to be about making a standalone cache process. By now, you should have both a REST and a
+Gateway process ready. Before, we start handling events we should build a Cache handler.
+
+## Why Use Standalone Cache Process?
+
+A standalone cache process allows you to retain cached data even after bot restarts. For example, if you are caching
+member roles to track when a role was added or removed, you may want to cache the members. The question then comes to
+play, when deciding where to keep your cache. Another reason to use this is, whether or not you are using a standalone
+gateway process.
+
+- Start rest process
+- Start event handler process (bot)
+- Start gateway process.
+ - Guild create events arrive providing all the data needed to cache in the bot process.
+- Restart event handler process(maybe for an update/reboot)
+ - You lost all guilds/channels/permissions etc and can not get them again without restarting gateway. This defeats the
+ entire point of the standalone gateway.
+
+If your cache is tied to the bot processes which is not tied to the gateway you lose all this info. The next thought is
+to just keep the Cache entirely in the gateway process however, I do not like this personally however, should you desire
+this you can do this as well. The reason I prefer not to do this is when your bot needs to make requests to your cache,
+you do not want it occupying the thread for processing other gateway events arriving from discord. A separate cache
+process makes it so it uses an entirely separate thread and will not slow down anything else.
+
+## Understand Cache Types
+
+When I use the term cache process, this is interchangeable with any similar term such as "custom cache", "redis cache",
+"pgsql cache", etc... The fact is you can keep this "cache" anywhere. For this guide, we will implement a very simple
+cache using pgsql. Feel free to modify this any way you like as advanced as you like. The point is Discordeno cache is
+flexible enough to let you use anything for your Cache storage.
+
+## Setting Up The Cache
+
+This step is for you to create the base schema for your cache. For example, if you want to implement a pgsql or redis
+cache perhaps you want to prepare the tables/schema. For this guide, we are just going to do a quick little hack to get
+a custom cache working.
+
+Create a file in a path like `src/bot/cache/schema.sql`
+
+```sql
+CREATE TABLE IF NOT EXISTS "users" (
+ id bigint NOT NULL,
+ username text COLLATE pg_catalog."default" NOT NULL,
+ discriminator text COLLATE pg_catalog."default" NOT NULL,
+ bot boolean,
+ CONSTRAINT "users_pkey" PRIMARY KEY (id)
+)
+```
+
+Note that you can cache only properties you want and leave all other properties that you won't use.
+
+Now that we have this schema ready for our users cache. Go ahead and repeat this for all other cache tables.
+
+Cache Tables:
+
+- users
+- members
+- guilds
+- channels
+- threads
+- messages
+- presences
+- unavailableGuilds
+
+Once you are finished continue forward, for the purpose of keeping this guide short we wont cover each table.
+
+> You should also run this file to prepare your pgsql and have your pgsql database running by now. Or whatever, cache
+> service you use.
+
+### Cache Handler
+
+Now we will initiate our cache service. This may be different for you based on your choice of cache type. Since we are
+using PGSQL for our cache layer, we will now instantiate it.
+
+```ts
+import { postgres } from "../../../deps.ts";
+
+// YOU CUSTOM PGSQL INFO GOES HERE
+const DATABASE_USERNAME = "";
+const DATABASE_PASSWORD = "";
+const DATABASE_NAME = "";
+const DATABASE_HOST = "";
+const DATABASE_PORT = 8956;
+const DATABASE_MAX = 20;
+
+export const psql = postgres({
+ username: DATABASE_USERNAME,
+ password: DATABASE_PASSWORD,
+ database: DATABASE_NAME,
+ host: DATABASE_HOST,
+ port: DATABASE_PORT,
+ max: DATABASE_MAX,
+ /*onnotice: (data) => {
+ logger.psql(`${data.severity} ${bgBrightBlack(`[${data.code}| ${data.file}:${data.line}]`)}`, data.message);
+ },*/
+ types: {
+ bigint: postgres.BigInt,
+ },
+});
+```
+
+To use the PGSQL driver we are using in this guide you can insert this into your `deps.ts`.
+
+```ts
+// @deno-types="https://denopkg.com/porsager/postgres@e2a8595d7aa8c3c838b83b9bca7b890c1707ad2c/types/index.d.ts"
+export { default as postgres } from "https://denopkg.com/porsager/postgres@e2a8595d7aa8c3c838b83b9bca7b890c1707ad2c/deno/lib/index.js";
+```
+
+> Note: Remember you can use any driver you like. For deno users we prefer to use this library for PGSQL because it is
+> more stable and more performant.
+
+Now that the cache layer is ready, we can proceed to begin creating our bot.
diff --git a/apps/site/tutorial/big-bot-guide/events.md b/apps/site/tutorial/big-bot-guide/events.md
new file mode 100644
index 000000000..2abb18f3f
--- /dev/null
+++ b/apps/site/tutorial/big-bot-guide/events.md
@@ -0,0 +1,260 @@
+---
+sidebar_position: 5
+sidebar_label: Step 4 - Event Handler
+---
+
+# Step 4: Creating Standalone Event Handler
+
+Now we are about to start working on the bot code itself. The last 3 steps should be completed by the time you reach
+this. The event handler process will be listening for events from any number of gateway instances and be ready to handle
+them.
+
+In this guide, we may use the term `Bot` or the term `event handler`, remember that these refer to the same thing. This
+is your main bot code.
+
+## Why Use Standalone Event Handler Process?
+
+The standalone event handler is the portion of your bot code that you will be changing the most. The three previous
+steps created processes that are intended to never be turned off. This process is designed to let you restart whenever
+you wish and be incredibly quick to restart. Since we don't have the delay to start up shards anymore, your code becomes
+reloaded instantly.
+
+## Creating Event Handlers
+
+Create a file path like `src/bot/mod.ts`.
+
+```ts
+import { DISCORD_TOKEN } from "../../configs.ts";
+import { Collection, createBot, Intents } from "../../deps.ts";
+import { psql } from "./cache/mod.ts";
+
+export const bot = createBot({
+ token: DISCORD_TOKEN,
+ botId: 270010330782892032n,
+ intents: Intents.Guilds | Intents.GuildMessages,
+ events: {
+ messageCreate: function (bot, message) {
+ console.log("message arrived");
+ },
+ },
+});
+```
+
+Alright that was a lot of code. Now let's break it down little by little.
+
+### Understanding createBot()
+
+**Basic Keys**
+
+- `token` if you can't figure this out stop reading and find another guide please. Thanks.
+- `botId` This is going to be your bot id. The reason we require this here is because we are going to set up a
+ standalone gateway process. With most other libs, they can fill this information using the READY event. However, since
+ our gateway is designed not to reboot, we are not going to get the READY event whenever we restart our bot. This means
+ we won't be able to fill this information later. Another method to get the id is to use the `token` but discord
+ developers have mentioned that this behavior is not documented and not supposed to be relied on to remain stable. Due
+ to these reasons, we chose to just require the bot id be passed here.
+- `applicationId` is an optional choice if your bot is old and has a unique id different from it's bot id.
+- `intents`: Provide the intents you like using a bitwise OR operation (eg. `Intents.Guilds | Intents.GuildsMessages`).
+ String form supports autocomplete and type safety.
+- `events`: These are your event handler functions. When a MESSAGE_CREATE event arrives from Discord it will be
+ processed here. We will set up the routing to run these functions later in the guide but for now you can see how to
+ set it up. Note, you can create these functions in separate files and just import them here as you wish.
+
+## Using Your Cache
+
+Since we are using a standalone gateway, a custom cache is essentially required as explained in step 3 of this guide.
+Here we'll have some basic functions to make use of the cache we created in step 3.
+
+```ts
+const cache = {
+ /** Get a single item from the table */
+ async get(key) {
+ return await psql`SELECT * FROM ${
+ psql(
+ tables[table],
+ )
+ } WHERE "id" = ${psql.types.bigint(key)}`;
+ },
+ /** Completely empty this table. */
+ async clear() {
+ await psql`TRUNCATE TABLE ${psql(tables[table])}`;
+ },
+ /** Delete the data related to this key from table. */
+ async delete(key) {
+ await psql`DELETE FROM ${
+ psql(
+ tables[table],
+ )
+ } WHERE "id" = ${psql.types.bigint(key)}`;
+ return true;
+ },
+ /** Check if there is data assigned to this key. */
+ async has(key) {
+ return Boolean(
+ await psql`SELECT 1 FROM ${
+ psql(
+ tables[table],
+ )
+ } WHERE "id" = ${psql.types.bigint(key)}`,
+ );
+ },
+ /** Check how many items are stored in this table. */
+ async size() {
+ return (await psql`SELECT COUNT("id") FROM ${psql(tables[table])}`)
+ .count;
+ },
+ /** Store new data to this table. */
+ async set(key, data) {
+ await psql`INSERT INTO ${psql(tables[table])} ${
+ psql(
+ data,
+ ...Object.keys(data),
+ )
+ }`;
+ return true;
+ },
+ // THESE TWO ARE USELESS FOR CUSTOM CACHE BUT NEED TO SHUT UP TS ERRORS
+ async forEach(callback) {},
+ async filter(callback) {
+ return new Collection();
+ },
+};
+```
+
+You can insert any code you desire for your cache system here. Since we were using PGSQL, we used sql queries to make
+these requests. However, should you need to communicate to Redis or anything else of your choice, you can do so here.
+
+> Note: The .filter() and .forEach() methods are unnecessary and should not be used for your bot as they are not
+> optimized for performance. These are made for smaller bot users who would not leave itoh alone and in order to please
+> them itoh gave them their hearts desire! LMAO!
+
+## Customizing Internal Code
+
+One of the best parts about discordeno is the flexibility. In order to show this off, we will use the `user` example but
+you can apply this to any part of the library.
+
+### Why Is Customizing Important?
+
+At large scale, every single property can become expensive to store in your cache. For example, if your bot does not
+make use of a `channel.topic` why storing potentially millions of strings in your memory for something you never
+need/user. This could save you potentially GBs of memory to just remove this one property.
+
+### Customizing Process
+
+First, let's create a file in some path like `src/bot/internals/mod.ts`. Note that we will create quite a few files
+below simply to keep code cleaner and simpler, in expectation that it will grow more complex later. You can merge them
+as you wish.
+
+```ts
+import { Bot } from "../../../deps.ts";
+import { customizeBotTransformers } from "./transformers/mod.ts";
+
+export function customizeBotInternals(bot: Bot) {
+ bot = customizeBotTransformers(bot);
+ // ADD AS MANY MORE CUSTOMIZATIONS HERE AS YOU LIKE TO HANDLERS, HELPERS, UTILS ETC...
+ return bot;
+}
+```
+
+We also need to add another file now at `src/bot/internals/transformers/mod.ts`
+
+```ts
+import { Bot } from "../../../../deps.ts";
+import { customizeUserTransformer } from "./user.ts";
+
+export function customizeBotTransformers(bot: Bot) {
+ bot = customizeUserTransformer(bot);
+ // ADD ANY MORE CUSTOM TRANSFORMERS HERE
+ return bot;
+}
+```
+
+One more file at `src/bot/internals/transformers/user.ts`
+
+```ts
+import { Bot, DiscordenoUser, transformUser } from "../../../../deps.ts";
+
+export function customizeUserTransformer(bot: Bot) {
+ bot.transformers.user = function (bot, payload) {
+ // REMOVE USELESS PROPS OUR BOT DOESNT USE
+ const { system, locale, verified, email, flags, mfaEnabled, premiumType, publicFlags, ...user } = transformUser(
+ bot,
+ payload,
+ );
+
+ // RETURN ONLY USEFUL PROPS WE NEED TO USE AND CACHE IF NECESSARY
+ return user as DiscordenoUser;
+ };
+
+ return bot;
+}
+```
+
+First we override the internal transformer for the `user` object. What's cool is the typings will be automatically
+provided :) Next, we use the `transformUser` function from the lib itself to make it create the internal user version.
+The reason I do this is so when I update the library and a new property is added or removed i can simply update and get
+it. Should you desire maximum control you can remove this entirely and only have what you want no matter what discord
+sends. Discordeno gives you the ability to stay in control.
+
+This method can be applied to any transformer, helper function, gateway event handler, util function or any part of the
+library. Anything and everything is possible to override. You do NOT need to fork and modify the library ever and give
+yourself a headache trying to maintain your fork with updates.
+
+## Handling Incoming Gateway Events
+
+Remember, this is a separate process we need to make sure we are listening to incoming events from our gateway
+instances. Since we used http in our Gateway step, we can create an http listener here as well.
+
+Create a file in a path like `src/bot/gatewayEventsListener.ts`
+
+Now we should create a http listener, check for authorization in headers, run `bot.events.raw` and `bot.handlers[event]`
+
+```ts
+import { DiscordGatewayPayload } from "discordeno";
+import { EVENT_HANDLER_PORT, REST_AUTHORIZATION } from "../../configs.ts";
+
+const server = Deno.listen({ port: EVENT_HANDLER_PORT });
+
+// Connections to the server will be yielded up as an async iterable.
+for await (const conn of server) {
+ // In order to not be blocking, we need to handle each connection individually
+ // in its own async function.
+ handleRequest(conn);
+}
+
+async function handleRequest(conn: Deno.Conn) {
+ // This "upgrades" a network connection into an HTTP connection.
+ const httpConn = Deno.serveHttp(conn);
+ // Each request sent over the HTTP connection will be yielded as an async
+ // iterator from the HTTP connection.
+ for await (const requestEvent of httpConn) {
+ if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== requestEvent.request.headers.get("AUTHORIZATION")) {
+ return requestEvent.respondWith(
+ new Response(JSON.stringify({ error: "Invalid authorization key." }), {
+ status: 401,
+ }),
+ );
+ }
+
+ const json = (await requestEvent.request.json()) as {
+ message: DiscordGatewayPayload;
+ shardId: number;
+ };
+
+ // Run raw event.
+ bot.events.raw(bot, json.message, json.shardId);
+
+ if (json.message.t && json.message.t !== "RESUMED") {
+ // When a guild or something isn't in cache this will fetch it before doing anything else.
+ if (!["READY", "GUILD_LOADED_DD"].includes(json.message.t)) {
+ await bot.events.dispatchRequirements(bot, json.message, json.shardId);
+ }
+
+ // Run event function provided in bot.events
+ bot.handlers[json.message.t]?.(bot, json.message, json.shardId);
+ }
+
+ new Response(undefined, { status: 200 });
+ }
+}
+```
diff --git a/apps/site/tutorial/big-bot-guide/gateway.md b/apps/site/tutorial/big-bot-guide/gateway.md
new file mode 100644
index 000000000..7f79dd954
--- /dev/null
+++ b/apps/site/tutorial/big-bot-guide/gateway.md
@@ -0,0 +1,989 @@
+---
+sidebar_position: 3
+sidebar_label: Step 2 - Gateway
+---
+
+# Step 2: Creating A Standalone Gateway Process
+
+If you are reading this, you should have your REST process completed. We are going to need it here. This process will be
+connecting to discord's websockets which will send you all the events.
+
+Before, we dive into how, here is a quick summary of why you will want a standalone gateway process.
+
+## Why Use Standalone REST Process?
+
+- **Zero Downtime Updates**:
+
+ - Your bot can be updated in a matter of seconds. With normal sharding, you have to restart which also has to process
+ identifying all your shards with a 1/~5s rate limit. With WS handling moved to a proxy process, this allows you to
+ instantly get the bot code restarted without any concerns of delays. If you have a bot on 200,000 servers normally
+ this would mean a 20 minute delay to restart your bot if you made a small change and restarted.
+
+- **Zero Downtime Resharding**:
+
+ - Discord stops letting your bot get added to new servers at certain points in time. For example, suppose you had
+ 150,000 servers running 150 shards. The maximum amount of servers your shards could hold is 150 \* 2500 = 375,000.
+ If your bot reaches this, it can no longer join new servers until it re-shards.
+ - DD proxy provides 2 types of re-sharding. Automated and manual. You can also have both.
+ - `Automated`: This system will automatically begin a Zero-downtime resharding process behind the scenes when you
+ reach 80% of your maximum servers allowed by your shards. For example, since 375,000 was the max, at 300,000 we
+ would begin re-sharding behind the scenes with `ZERO DOWNTIME`.
+ - 80% of maximum servers reached (The % of 80% is customizable.)
+ - Identify limits have room to allow re-sharding. (Also customizable)
+ - `Manual`: You can also trigger this manually should you choose.
+
+- **Horizontal Scaling**:
+
+ - The proxy system allows you to scale the bot horizontally. When you reach a huge size, you can either keep spending
+ more money to keep beefing up your server or you can buy several cheaper servers and scale horizontally. The proxy
+ means you can have WS handling on a completely separate system.
+
+- **No Loss Restarts**:
+
+ - When you restart a bot without the proxy system, normally you would lose many events. Users may be using commands or
+ messages are sent that will not be filtered. As your bot's grow this number rises dramatically. Users may join who
+ wont get the auto-roles or any other actions your bot should take. With the proxy system, you can keep restarting
+ your bot and never lose any events. Events will be put into a queue while your bot is down(max size of queue is
+ customizable), once the bot is available the queue will begin processing all events.
+
+- **Controllers**:
+
+ - The controller aspect gives you full control over everything inside the proxy. You can provide a function to simply
+ override the handler. For example, if you would like a certain function to do something different, instead of having
+ to fork and maintain your fork, you can just provide a function to override.
+
+- **Clustering With Workers**:
+ - Take full advantage of all your CPU cores by using workers to spread the load. Control how many shards per worker
+ and how many workers to maximize efficiency!
+
+## Creating Gateway Manager
+
+Create a file under some path like `src/gateway/mod.ts`.
+
+```ts
+import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from "../../configs.ts";
+import { BASE_URL, createRestManager } from "../../deps.ts";
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: `http://localhost:${REST_PORT}`,
+});
+```
+
+Throw another rest manager here which will be responsible for calling the main REST process we created in Step 1. This
+will allow your gateway to communicate to the other process. Remember this is just to communicate outwards, this file
+should not have the http listener.
+
+> Feel free to refactor and optimize this should you wish to move `const rest...` to a separate file and reuse in both
+> steps.
+
+### Getting Gateway Bot Data
+
+Now we need to use this rest manager to call the api to get information about how to connect to discord's gateway for
+your bot.
+
+```ts
+import { routes } from "../../deps.ts";
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: `http://localhost:${REST_PORT}`,
+});
+
+// CALL THE REST PROCESS TO GET GATEWAY DATA
+const gatewayBot = await rest.runMethod(rest, "GET", routes.GATEWAY_BOT()).then((res) => ({
+ url: res.url,
+ shards: res.shards,
+ sessionStartLimit: {
+ total: res.session_start_limit.total,
+ remaining: res.session_start_limit.remaining,
+ resetAfter: res.session_start_limit.reset_after,
+ maxConcurrency: res.session_start_limit.max_concurrency,
+ },
+}));
+```
+
+With this info, we can now create our gateway manager.
+
+### Understanding Gateway Manager
+
+```ts
+import { INTENTS, SHARDS_PER_WORKER, TOTAL_WORKERS } from "../../configs.ts";
+
+const gateway = createGatewayManager({
+ gatewayBot,
+ gatewayConfig: {
+ token: DISCORD_TOKEN,
+ intents: INTENTS,
+ },
+ totalShards: gatewayBot.shards,
+ shardsPerWorker: SHARDS_PER_WORKER,
+ totalWorkers: TOTAL_WORKERS,
+ // debug: console.log,
+ // THIS WILL BE USED LATER IN WORKER SO LEAVE IT HERE
+ handleDiscordPayload: () => {},
+});
+```
+
+#### Basic Keys
+
+- `EVENT_HANDLER_SECRET_KEY` is from your configs that will be used to make sure requests sent to your event handler
+ process are indeed from you.
+- `DISCORD_TOKEN` if you can't figure this out, this guide isn't for you. Please find another.
+- `INTENTS` pass in a number or a string of intents. Autocomplete/type-safety is provided for strings :)
+
+#### Powerful Keys
+
+If your bot is going to be run on one process, you can re-use the data that discord gave you to connect.
+
+- `totalShards`: is the maximum number of shards you want to use for connecting your bot. Should you think Discord is
+ not smart enough to recommend a good amount, use this to override their choice. Highly recommend just using theirs.
+- `lastShardId`: is the last shard you want to connect in this process.
+ - Using a combination of `lastShardId` & `firstShardId`, you can create several processes or even several servers to
+ handle different amounts of shards should your bot get that big to require horizontal scaling. You can control how
+ many shards each gateway manager will be responsible for.
+- `spawnShardDelay`: The delay in milliseconds to wait before spawning next shard.
+- `shardsPerWorker`: The amount of shards to load per worker. Discussed in detail below.
+- `totalWorkers`: The maximum amount of workers to use for your bot.
+
+#### Gateway Cache
+
+There is a few things that we cache in the gateway process directly, because sending them across the network is not
+ideal. This is done to support custom cache functionality.
+
+- `guildIds`: Used for determining what type of GUILD_CREATE event is received.
+- `loadingGuildIds`: Used for determining if all guilds have arrived when initially connecting.
+- `editedMessages`: Used to prevent spam of events across the network. MESSAGE_UPDATE are an extremely heavy event. Any
+ embed or link that is in a message will unfurl triggerring a message update event. This is undesired behavior for 99%
+ of bots out there. If someone sends a message with 5 urls, in there you will get a MESSAGE_CREATE and 5 MESSAGE_UPDATE
+ events. If that user edits a single letter on it you now get 6 MESSAGE_UPDATE events, 1 for the content change and 5
+ more for each url being unfurled. The editedMessages cache checks if the content of the message changed or not before
+ sending the event downstream. Override this behavior if you need different behavior.
+
+#### Gateway Method Overriding
+
+One of the benefits of Discordeno is that you can override/customize anything from the library. Should you desire to
+change the logic in any method it is as simple as:
+
+```ts
+// TYPINGS WILL BE AUTOMATICALLY PROVIDED
+gateway.heartbeat = function (gateway, shardId, interval) {
+ // YOUR CUSTOM HANDLING CODE HERE
+};
+```
+
+## Workers
+
+Now, we should take a minute here to talk about workers. Workers are just Clusters in Node.js
+
+When you have a big bot and you are processing millions of events, you need to speed up that processing. Keeping it in 1
+thread is not very nice since JavaScript is single threaded. This means it can only process 1 event at a time. With
+workers, you can make it process several events at the same time. We mentioned the `shardsPerWorker` earlier. This
+option was added to allow you to choose how many shards should be managed by each worker.
+
+When shards are spawned, they are triggered by a method on gateway: `tellWorkerToIdentify`, so we'll have to modify it
+to create workers and send message:
+
+```ts
+gateway.tellWorkerToIdentify = async (_gateway, workerId, shardId, _bucketId) => {
+ let worker = workers.get(workerId);
+ if (!worker) {
+ worker = createWorker(workerId);
+ workers.set(workerId, worker);
+ }
+
+ // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW
+ const identify: WorkerMessage = {
+ type: "IDENTIFY_SHARD",
+ shardId,
+ };
+
+ worker.postMessage(identify);
+};
+```
+
+You can choose to replace the handler with any desired functionality you like. For example, should should you want to
+create a new worker for each new workerId that appears and have that worker trigger the identify functionaly. How you
+choose to handler workers is left in your care.
+
+Now that we've setup our initial gateway manager and added `tellWorkerToIdentify` to `gateway`, we need to do the rest
+of the work: creating workers, spawning shards etc.
+
+```ts
+import { EVENT_HANDLER_SECRET_KEY, EVENT_HANDLER_URL } from "../../configs.ts";
+import { Worker } from "worker_threads";
+import { WorkerCreateData, WorkerGetShardInfo, WorkerMessage, WorkerShardInfo, WorkerShardPayload } from "./worker.js";
+
+// A COLLECTION OF WORKERS
+const workers = new Collection();
+const nonces = new Collection void>();
+
+function createWorker(workerId: number) {
+ const workerData: WorkerCreateData = {
+ intents: gateway.manager.gatewayConfig.intents ?? 0,
+ token: DISCORD_TOKEN,
+ // TODO: PUT THIS SEPARATELY. CAN USE MULTIPLE URLS IF YOU HAVE MULTIPLE BOT PROCESSES HANDLING DIFFERENT SHARDS' EVENTS
+ handlerUrls: [EVENT_HANDLER_URL],
+ handlerAuthorization: EVENT_HANDLER_SECRET_KEY,
+ path: "./worker.ts",
+ totalShards: gateway.manager.totalShards,
+ workerId,
+ };
+
+ const worker = new Worker("./worker.js", {
+ workerData,
+ });
+
+ worker.on("message", async (data: ManagerMessage) => {
+ switch (data.type) {
+ case "REQUEST_IDENTIFY": {
+ await gateway.manager.requestIdentify(data.shardId);
+
+ const allowIdentify: WorkerMessage = {
+ type: "ALLOW_IDENTIFY",
+ shardId: data.shardId,
+ };
+
+ worker.postMessage(allowIdentify);
+
+ break;
+ }
+ case "NONCE_REPLY": {
+ nonces.get(data.nonce)?.(data.data);
+ }
+ }
+ });
+
+ return worker;
+}
+
+// TYPES WE USE
+export type ManagerMessage = ManagerRequestIdentify | ManagerNonceReply;
+
+export type ManagerRequestIdentify = {
+ type: "REQUEST_IDENTIFY";
+ shardId: number;
+};
+
+export type ManagerNonceReply = {
+ type: "NONCE_REPLY";
+ nonce: string;
+ data: T;
+};
+```
+
+## Spawning Shards
+
+Once you are ready and the gateway has been created as you desired, we can begin spawning the shards.
+
+```ts
+gateway.spawnShards(gateway);
+```
+
+This code now handles creating gateway manager, creating workers, spawning shards and sending the info to each workers.
+Next is creating a worker file to receive these info, connecting to gateway and sending the events to bot process.
+
+Here's the full code of `src/gateway/mod.ts`:
+
+```ts
+import {
+ DISCORD_TOKEN,
+ EVENT_HANDLER_SECRET_KEY,
+ EVENT_HANDLER_URL,
+ INTENTS,
+ REST_AUTHORIZATION,
+ REST_PORT,
+ SHARDS_PER_WORKER,
+ TOTAL_WORKERS,
+} from "../../configs.ts";
+import { BASE_URL, createRestManager, routes } from "../../deps.ts";
+import { Worker } from "worker_threads";
+import { WorkerCreateData, WorkerGetShardInfo, WorkerMessage, WorkerShardInfo, WorkerShardPayload } from "./worker.ts";
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: `http://localhost:${REST_PORT}`,
+});
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: `http://localhost:${REST_PORT}`,
+});
+
+// CALL THE REST PROCESS TO GET GATEWAY DATA
+const gatewayBot = await rest.runMethod(rest, "GET", routes.GATEWAY_BOT()).then((res) => ({
+ url: res.url,
+ shards: res.shards,
+ sessionStartLimit: {
+ total: res.session_start_limit.total,
+ remaining: res.session_start_limit.remaining,
+ resetAfter: res.session_start_limit.reset_after,
+ maxConcurrency: res.session_start_limit.max_concurrency,
+ },
+}));
+
+const gateway = createGatewayManager({
+ gatewayBot,
+ gatewayConfig: {
+ token: DISCORD_TOKEN,
+ intents: INTENTS,
+ },
+ totalShards: gatewayBot.shards,
+ shardsPerWorker: SHARDS_PER_WORKER,
+ totalWorkers: TOTAL_WORKERS,
+ // debug: console.log,
+ handleDiscordPayload: () => {},
+ tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => {
+ let worker = workers.get(workerId);
+ if (!worker) {
+ worker = createWorker(workerId);
+ workers.set(workerId, worker);
+ }
+
+ // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW
+ const identify: WorkerMessage = {
+ type: "IDENTIFY_SHARD",
+ shardId,
+ };
+
+ worker.postMessage(identify);
+ },
+});
+
+// A COLLECTION OF WORKERS
+const workers = new Collection();
+const nonces = new Collection void>();
+
+function createWorker(workerId: number) {
+ const workerData: WorkerCreateData = {
+ intents: gateway.manager.gatewayConfig.intents ?? 0,
+ token: DISCORD_TOKEN,
+ handlerUrls: [EVENT_HANDLER_URL],
+ handlerAuthorization: EVENT_HANDLER_SECRET_KEY,
+ path: "./worker.ts",
+ totalShards: gateway.manager.totalShards,
+ workerId,
+ };
+
+ const worker = new Worker("./worker.ts", {
+ workerData,
+ });
+
+ worker.on("message", async (data: ManagerMessage) => {
+ switch (data.type) {
+ case "REQUEST_IDENTIFY": {
+ await gateway.manager.requestIdentify(data.shardId);
+
+ const allowIdentify: WorkerMessage = {
+ type: "ALLOW_IDENTIFY",
+ shardId: data.shardId,
+ };
+
+ worker.postMessage(allowIdentify);
+
+ break;
+ }
+ case "NONCE_REPLY": {
+ nonces.get(data.nonce)?.(data.data);
+ }
+ }
+ });
+
+ return worker;
+}
+
+// TYPES WE USE
+export type ManagerMessage = ManagerRequestIdentify | ManagerNonceReply;
+
+export type ManagerRequestIdentify = {
+ type: "REQUEST_IDENTIFY";
+ shardId: number;
+};
+
+export type ManagerNonceReply = {
+ type: "NONCE_REPLY";
+ nonce: string;
+ data: T;
+};
+
+// SPAWN SHARDS INTO WORKERS
+gateway.spawnShards();
+```
+
+## Worker File
+
+Now that we've handled creating gateway, workers, we need to create a worker file to identify, receive gateway events
+and send them to bot process.
+
+Create a file in a path like `src/gateway/worker.ts`.
+
+Now we'll have to create a Shard Manager, this is what will handle identifying, receiving events.
+
+```ts
+import { createShardManager } from "discordeno";
+import { parentPort, workerData } from "worker_threads";
+
+if (!parentPort) {
+ throw new Error("Parent port is null");
+}
+
+// THE DATA WE GET FROM GATEWAY FILE
+const script: WorkerCreateData = workerData;
+
+const identifyPromises = new Map void>();
+
+const manager = createShardManager({
+ gatewayConfig: {
+ intents: script.intents,
+ token: script.token,
+ },
+ shardIds: [],
+ totalShards: script.totalShards,
+ // WE WILL COVER THESE TWO FUNCTIONS IN LATER PART OF THE GUIDE, FOR NOW, LEAVE IT THIS WAY
+ handleMessage: () => {},
+ requestIdentify: async () => {},
+});
+```
+
+The above code only creates a shard manager, we now have 3 more things to do:
+
+- Listening to gateway process, sending events received to respective shards in the manager.
+- Handling Discord Payloads.
+- Requesting Identify.
+
+## Sending events from Gateway to Shards in Manager
+
+In order for the shards to receive events and send to bot process, we need to receive the event payloads from gateway
+first, we can do this by using `message` event in `parentPort` like shown below:
+
+```ts
+import { Shard } from "discordeno";
+
+function buildShardInfo(shard: Shard): WorkerShardInfo {
+ return {
+ workerId: script.workerId,
+ shardId: shard.id,
+ rtt: shard.heart.rtt || -1,
+ state: shard.state,
+ };
+}
+
+parentPort.on("message", async (data: WorkerMessage) => {
+ switch (data.type) {
+ // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify
+ case "IDENTIFY_SHARD": {
+ await manager.identify(data.shardId);
+
+ break;
+ }
+ // Gateway sends ALLOW_IDENTIFY when worker requests to identify
+ case "ALLOW_IDENTIFY": {
+ identifyPromises.get(data.shardId)?.();
+ identifyPromises.delete(data.shardId);
+
+ break;
+ }
+ // Gateway sends SHARD_PAYLOAD for every events it receives from Discord
+ case "SHARD_PAYLOAD": {
+ manager.shards.get(data.shardId)?.send(data.data);
+
+ break;
+ }
+ // Send shard info if gateway sends GET_SHARD_INFO
+ case "GET_SHARD_INFO": {
+ const infos = manager.shards.map(buildShardInfo);
+
+ parentPort?.postMessage({ type: "NONCE_REPLY", nonce: data.nonce, data: infos });
+ }
+ }
+});
+```
+
+Now TypeScript will error because of missing types, add these to your code:
+
+```ts
+import { ShardSocketRequest, ShardState } from "discordeno";
+
+export type WorkerMessage = WorkerIdentifyShard | WorkerAllowIdentify | WorkerShardPayload | WorkerGetShardInfo;
+
+export type WorkerIdentifyShard = {
+ type: "IDENTIFY_SHARD";
+ shardId: number;
+};
+
+export type WorkerAllowIdentify = {
+ type: "ALLOW_IDENTIFY";
+ shardId: number;
+};
+
+export type WorkerShardPayload = {
+ type: "SHARD_PAYLOAD";
+ shardId: number;
+ data: ShardSocketRequest;
+};
+
+export type WorkerGetShardInfo = {
+ type: "GET_SHARD_INFO";
+ nonce: string;
+};
+
+export type WorkerCreateData = {
+ intents: number;
+ token: string;
+ handlerUrls: string[];
+ handlerAuthorization: string;
+ path: string;
+ totalShards: number;
+ workerId: number;
+};
+
+export type WorkerShardInfo = {
+ workerId: number;
+ shardId: number;
+ rtt: number;
+ state: ShardState;
+};
+```
+
+## Handling Discord Payloads
+
+One of the big things we didn't cover yet is the handler for discord payloads. This is the main sauce of your worker
+process here. This is going to take the events that the gateway manager sent and send it to your event handler. How you
+wish to communicate with your event handler is up to you. For this guide, we will use http, but you can replace that
+with anything you like.
+
+```ts
+manager.createShardOptions.handleMessage = async (shard, message) => {
+ const url = script.handlerUrls[shard.id % script.handlerUrls.length];
+ if (!url) return console.error("ERROR: NO URL FOUND TO SEND MESSAGE");
+
+ await fetch(url, {
+ method: "POST",
+ body: JSON.stringify({ message, shardId: shard.id }),
+ headers: { "Content-Type": "application/json", Authorization: script.handlerAuthorization },
+ }).catch((error) => console.error(error));
+};
+```
+
+You can change this function to use a WS or any form of communication you prefer to use to send this to your event
+handler.
+
+This is also the place where you make use of the [Gateway Cache](#gateway-cache) we mentioned earlier (`guildIds`,
+`loadingGuildIds`, `editedMessages`).
+
+## Gateway Queue
+
+One thing we can add on here, which you will find already done in the template if you are using it. However, it is still
+good to read this to learn and understand the logic behind it. When you need a downtime for whatever reason, you can
+create a queue like system to avoid any missed events. Let's create a simple queue. If it errors, assuming something
+like the bot event listener process is down for whatever reason, the `.catch` in `fetch` will run adding this event to
+the queue to try again in one second by calling the `handleQueue` function.
+
+```ts
+.catch(() => {
+ // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN
+ if (message.t === "INTERACTION_CREATE") handleInteractionQueueing(message, shard.id);
+ else queue.events.push({ shardId: shard.id, message });
+
+ setTimeout(handleQueue, 1000);
+});
+```
+
+Now TypeScript will probably throw some errors at your face, so let's fix those real quick. Create an object that will
+hold the queue of events for our gateway.
+
+```ts
+import { DiscordGatewayPayload } from "discordeno";
+
+const queue: GatewayQueue = {
+ processing: false,
+ events: [],
+};
+
+export interface QueuedEvent {
+ message: DiscordGatewayPayload;
+ shardId: number;
+}
+
+export interface GatewayQueue {
+ processing: boolean;
+ events: QueuedEvent[];
+}
+
+async function handleQueue() {
+ // PLACEHOLDER FUNCTION THAT WILL HANDLE PROCESSING THE QUEUE
+}
+
+async function handleInteractionQueueing(message: DiscordGatewayPayload, shardId: number) {
+ // PLACEHOLDER FUNCTION
+}
+```
+
+Alrighty, since TypeScript stopped being annoying, let's continue. Next, we should make sure to avoid fetching when the
+queue is already processing or has events queued up. This will help us preserve the order of events in the queue.
+
+```ts
+handleMessage: async function (shard, message) {
+// IF QUEUE IS RUNNING JUST ADD TO QUEUE
+if (queue.processing) {
+ if (message.t === "INTERACTION_CREATE") return handleInteractionQueueing(message, shard.id);
+
+ return queue.events.push({ shardId: shard.id, message });
+}
+
+await fetch(EVENT_HANDLER_URL, {
+```
+
+Typescript must be at it again so let's shut it up again. Keep in mind that we are handling interaction events
+separately because they require a response within 3 seconds or they will become invalid. In this function first we
+automatically respond to the ones that can not be deferred. For the interactions that can be deferred, we will simply
+defer them and add this event to the queue.
+
+```ts
+import { DiscordInteraction, InteractionResponseTypes, InteractionTypes, routes } from "discordeno";
+import { BOT_SERVER_INVITE_CODE } from "../../configs.ts";
+
+async function handleInteractionQueueing(message: DiscordGatewayPayload, shardId: number) {
+ if (message.t !== "INTERACTION_CREATE") return;
+
+ const interaction = message.d as DiscordInteraction;
+ // IF THIS INTERACTION IS NOT DEFERABLE
+ if ([InteractionTypes.ModalSubmit, InteractionTypes.ApplicationCommandAutocomplete].includes(interaction.type)) {
+ return await rest.runMethod(
+ rest,
+ "POST",
+ routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token),
+ {
+ type: InteractionResponseTypes.ChannelMessageWithSource,
+ data: {
+ content:
+ `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`,
+ },
+ },
+ );
+ }
+
+ await rest.runMethod(rest, "POST", endpoints.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), {
+ // MESSAGE COMPONENTS NEED SPECIAL DEFER
+ type: InteractionTypes.MessageComponent === interaction.type
+ ? InteractionResponseTypes.DeferredUpdateMessage
+ : InteractionResponseTypes.DeferredChannelMessageWithSource,
+ });
+
+ // ADD EVENT TO QUEUE
+ queue.events.push({ shardId, message });
+}
+```
+
+Oh no, TypeScript is at it again. We need to make a REST manager so that our gateway proxy can communicate with our REST
+proxy. We then can make use of it to send a POST request.
+
+```ts
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: REST_URL,
+});
+```
+
+So now there is only one thing left the `handleQueue` function. First we get the first item from the queue using
+`.shift()`. Then we check to see if that item exists. If it does not exist, we mark the queue as no longer processing
+and cancel out. However, if it does exist, we send a fetch request to the bot event handler process. In the `.catch()`
+we will add this event back in to the start of the queue in case the bot is still down. Finally we call this function
+again to run the next item in the queue.
+
+```ts
+async function handleQueue() {
+ const event = queue.events.shift();
+ // QUEUE IS EMPTY
+ if (!event) {
+ console.log("GATEWAY QUEUE ENDING");
+ queue.processing = false;
+ return;
+ }
+
+ await fetch(EVENT_HANDLER_URL, {
+ headers: {
+ Authorization: EVENT_HANDLER_SECRET_KEY,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ body: JSON.stringify({
+ shardId: event.shardId,
+ message: event.message,
+ }),
+ })
+ .then((res) => {
+ res.text();
+ handleQueue();
+ })
+ .catch(() => {
+ // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE
+ queue.events.unshift(event);
+ setTimeout(handleQueue, 1000);
+ });
+}
+```
+
+## Requesting Identify
+
+We need to request identify in order to trigger initial handshake with the gateway, we'll use `manager.requestIdentify`
+to do this.
+
+```ts
+import { ManagerMessage } from "./mod.ts";
+
+manager.requestIdentify = async function (shardId: number): Promise {
+ return await new Promise((resolve) => {
+ identifyPromises.set(shardId, resolve);
+
+ const identifyRequest: ManagerMessage = {
+ type: "REQUEST_IDENTIFY",
+ shardId,
+ };
+
+ parentPort?.postMessage(identifyRequest);
+ });
+};
+```
+
+That's all, you've now setup your gateway and worker. Here's the full code of `src/gateway/worker.ts`:
+
+```ts
+import {
+ createRestManager,
+ createShardManager,
+ DiscordGatewayPayload,
+ DiscordInteraction,
+ InteractionResponseTypes,
+ InteractionTypes,
+ routes,
+ Shard,
+ ShardSocketRequest,
+ ShardState,
+} from "discordeno";
+import { parentPort, workerData } from "worker_threads";
+import { ManagerMessage } from "./mod";
+import {
+ BOT_SERVER_INVITE_CODE,
+ DISCORD_TOKEN,
+ EVENT_HANDLER_SECRET_KEY,
+ EVENT_HANDLER_URL,
+ REST_AUTHORIZATION,
+ REST_URL,
+} from "../../configs.ts";
+
+if (!parentPort) {
+ throw new Error("Parent port is null");
+}
+
+// THE DATA WE GET FROM GATEWAY FILE
+const script: WorkerCreateData = workerData;
+
+const identifyPromises = new Map void>();
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: REST_URL,
+});
+
+const manager = createShardManager({
+ gatewayConfig: {
+ intents: script.intents,
+ token: script.token,
+ },
+ shardIds: [],
+ totalShards: script.totalShards,
+ handleMessage: async (shard, message) => {
+ const url = script.handlerUrls[shard.id % script.handlerUrls.length];
+ if (!url) return console.error("ERROR: NO URL FOUND TO SEND MESSAGE");
+
+ await fetch(url, {
+ method: "POST",
+ body: JSON.stringify({ message, shardId: shard.id }),
+ headers: { "Content-Type": "application/json", Authorization: script.handlerAuthorization },
+ }).catch(() => {
+ // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN
+ if (message.t === "INTERACTION_CREATE") handleInteractionQueueing(message, shard.id);
+ else queue.events.push({ shardId: shard.id, message });
+
+ setTimeout(handleQueue, 1000);
+ });
+ },
+ requestIdentify: async function (shardId: number): Promise {
+ return await new Promise((resolve) => {
+ identifyPromises.set(shardId, resolve);
+
+ const identifyRequest: ManagerMessage = {
+ type: "REQUEST_IDENTIFY",
+ shardId,
+ };
+
+ parentPort?.postMessage(identifyRequest);
+ });
+ },
+});
+
+function buildShardInfo(shard: Shard): WorkerShardInfo {
+ return {
+ workerId: script.workerId,
+ shardId: shard.id,
+ rtt: shard.heart.rtt || -1,
+ state: shard.state,
+ };
+}
+
+parentPort.on("message", async (data: WorkerMessage) => {
+ switch (data.type) {
+ // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify
+ case "IDENTIFY_SHARD": {
+ await manager.identify(data.shardId);
+
+ break;
+ }
+ // Gateway sends ALLOW_IDENTIFY when worker requests to identify
+ case "ALLOW_IDENTIFY": {
+ identifyPromises.get(data.shardId)?.();
+ identifyPromises.delete(data.shardId);
+
+ break;
+ }
+ // Gateway sends SHARD_PAYLOAD for every events it receives from Discord
+ case "SHARD_PAYLOAD": {
+ manager.shards.get(data.shardId)?.send(data.data);
+
+ break;
+ }
+ // Send shard info if gateway sends GET_SHARD_INFO
+ case "GET_SHARD_INFO": {
+ const infos = manager.shards.map(buildShardInfo);
+
+ parentPort?.postMessage({ type: "NONCE_REPLY", nonce: data.nonce, data: infos });
+ }
+ }
+});
+
+const queue: GatewayQueue = {
+ processing: false,
+ events: [],
+};
+
+async function handleQueue() {
+ const event = queue.events.shift();
+ // QUEUE IS EMPTY
+ if (!event) {
+ console.log("GATEWAY QUEUE ENDING");
+ queue.processing = false;
+ return;
+ }
+
+ await fetch(EVENT_HANDLER_URL, {
+ headers: {
+ Authorization: EVENT_HANDLER_SECRET_KEY,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ body: JSON.stringify({
+ shardId: event.shardId,
+ message: event.message,
+ }),
+ })
+ .then((res) => {
+ res.text();
+ handleQueue();
+ })
+ .catch(() => {
+ // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE
+ queue.events.unshift(event);
+ setTimeout(handleQueue, 1000);
+ });
+}
+
+async function handleInteractionQueueing(message: DiscordGatewayPayload, shardId: number) {
+ if (message.t !== "INTERACTION_CREATE") return;
+
+ const interaction = message.d as DiscordInteraction;
+ // IF THIS INTERACTION IS NOT DEFERABLE
+ if ([InteractionTypes.ModalSubmit, InteractionTypes.ApplicationCommandAutocomplete].includes(interaction.type)) {
+ return await rest.runMethod(rest, "POST", routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), {
+ type: InteractionResponseTypes.ChannelMessageWithSource,
+ data: {
+ content:
+ `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`,
+ },
+ });
+ }
+
+ await rest.runMethod(rest, "POST", routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), {
+ // MESSAGE COMPONENTS NEED SPECIAL DEFER
+ type: InteractionTypes.MessageComponent === interaction.type
+ ? InteractionResponseTypes.DeferredUpdateMessage
+ : InteractionResponseTypes.DeferredChannelMessageWithSource,
+ });
+
+ // ADD EVENT TO QUEUE
+ queue.events.push({ shardId, message });
+}
+
+export type WorkerMessage = WorkerIdentifyShard | WorkerAllowIdentify | WorkerShardPayload | WorkerGetShardInfo;
+
+export type WorkerIdentifyShard = {
+ type: "IDENTIFY_SHARD";
+ shardId: number;
+};
+
+export type WorkerAllowIdentify = {
+ type: "ALLOW_IDENTIFY";
+ shardId: number;
+};
+
+export type WorkerShardPayload = {
+ type: "SHARD_PAYLOAD";
+ shardId: number;
+ data: ShardSocketRequest;
+};
+
+export type WorkerGetShardInfo = {
+ type: "GET_SHARD_INFO";
+ nonce: string;
+};
+
+export type WorkerCreateData = {
+ intents: number;
+ token: string;
+ handlerUrls: string[];
+ handlerAuthorization: string;
+ path: string;
+ totalShards: number;
+ workerId: number;
+};
+
+export type WorkerShardInfo = {
+ workerId: number;
+ shardId: number;
+ rtt: number;
+ state: ShardState;
+};
+
+export interface QueuedEvent {
+ message: DiscordGatewayPayload;
+ shardId: number;
+}
+
+export interface GatewayQueue {
+ processing: boolean;
+ events: QueuedEvent[];
+}
+```
+
+Note, you can take this concept and expand on it as much as you like. You can swap out the fetch() with websockets or
+any other system you like to communicate between your processes. I highly recommend you take some time to add checks in
+place to prevent adding to queue when the queue reaches a certain size. You don't want this to become a memory leak of
+infinite size and crash your gateway. So take the time to do this right in your setup.
+
+If you have any questions, please contact us on our [discord server](https://discord.gg/ddeno).
diff --git a/apps/site/tutorial/big-bot-guide/rest.md b/apps/site/tutorial/big-bot-guide/rest.md
new file mode 100644
index 000000000..f33b3e137
--- /dev/null
+++ b/apps/site/tutorial/big-bot-guide/rest.md
@@ -0,0 +1,136 @@
+---
+sidebar_position: 2
+sidebar_label: Step 1 - REST
+---
+
+# Creating A Standalone REST Process
+
+The first thing we want to make is our standalone REST process. This process will be used by almost every other process,
+so it is going to be the foundation of the bot.
+
+Before, we dive into how, here is a quick summary of why you will want a standalone REST process.
+
+## Why Use Standalone REST Process?
+
+- Easily host on any serverless infrastructure.
+- Freedom from global rate limit errors
+ - As your bot grows, you want to handle global rate limits better. Shards don't communicate fast enough to truly
+ handle it properly so this allows 1 rest handler across the entire bot.
+ - In fact, you can host multiple instances of your bot and all connect to the same rest server.
+- REST does not rest!
+ - Separate rest means if your bot for whatever reason crashes, your requests that are queued will still keep going and
+ will not be lost.
+ - Seamless updates! When you want to update and reboot the bot, you could potentially lose tons of messages or
+ responses that are in queue. Using this you could restart your bot without ever worrying about losing any responses.
+- Single source of contact to Discord API
+ - This will allow you to make requests to discord from anywhere including a bot dashboard. You no longer need to have
+ to communicate to your bot processes just to make a request or anything. Free up your bot process for processing bot
+ events.
+- Scalability! Scalability! Scalability!
+
+## Preparations
+
+Before going further, you should have already made the following pieces:
+
+- rest/mod.ts
+- deps.ts (Make sure to import discordeno)
+- configs.ts
+- Deno extension(if you are using deno, this is required)
+- TabNine extension to make your life so much better. (Optional)
+
+## Creating Rest Manager
+
+Now let's open up that rest file and start coding.
+
+```ts
+import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from "../../configs.ts";
+import { BASE_URL, createRestManager } from "../../deps.ts";
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: `http://localhost:${REST_PORT}`,
+});
+```
+
+- `createRestManager` is imported from your deps file which should have exported everything from discordeno.
+- `DISCORD_TOKEN` is the bot's token itself.
+- `REST_AUTHORIZATION` is a special password you want to use to authenticate that requests being sent to your port are
+ indeed from you.
+- `customUrl` the url where this rest process will be running. This can be localhost which we are using in this guide if
+ you want all processes on same VPS or separate them to different servers for horizontal scaling. `REST_PORT` is just
+ the port where you want the process hosted.
+
+Now you have an entire Rest manager ready and waiting. Only thing you need now, is to listen for requests.
+
+## Creating HTTP Listener
+
+Since this is not a beginner guide, I am assuming you know already how to create a HTTP listener. There are enough
+guides on this out there. I will only cover the rough functionality.
+
+```ts
+// START LISTENING TO THE URL(localhost)
+const server = Deno.listen({ port: REST_PORT });
+console.info(
+ `HTTP webserver running. Access it at: http://localhost:${REST_PORT}/`,
+);
+
+// Connections to the server will be yielded up as an async iterable.
+for await (const conn of server) {
+ // In order to not be blocking, we need to handle each connection individually
+ // in its own async function.
+ handleRequest(conn);
+}
+
+async function handleRequest(conn: Deno.Conn) {
+ // This "upgrades" a network connection into an HTTP connection.
+ const httpConn = Deno.serveHttp(conn);
+ // Each request sent over the HTTP connection will be yielded as an async
+ // iterator from the HTTP connection.
+ for await (const requestEvent of httpConn) {
+ if (
+ !REST_AUTHORIZATION ||
+ REST_AUTHORIZATION !== requestEvent.request.headers.get("AUTHORIZATION")
+ ) {
+ return requestEvent.respondWith(
+ new Response(JSON.stringify({ error: "Invalid authorization key." }), {
+ status: 401,
+ }),
+ );
+ }
+
+ const json = (await requestEvent.request.json()) as any;
+
+ // IMPLEMENT ANY ERROR HANDLING HERE IF YOU WOULD LIKE BY WRAPPING THIS IN A CATCH
+
+ // MAKE THE REQUEST TO DISCORD
+ const result = await rest.runMethod(
+ rest,
+ // USE THE SAME METHOD THAT CAME IN. IF DELETE CAME IN WE SEND DELETE OUT
+ requestEvent.request.method as any,
+ // OVERWRITE THE CUSTOM URL WITH DISCORDS BASE URL
+ `${BASE_URL}/v${rest.version}${
+ requestEvent.request.url.substring(
+ rest.customUrl.length,
+ )
+ }`,
+ json,
+ );
+
+ // RETURN DISCORDS RESPONSE BACK TO THE PROCESS MAKING THE REQUEST
+ if (result) {
+ requestEvent.respondWith(
+ new Response(JSON.stringify(result), {
+ status: 200,
+ }),
+ );
+ } else {
+ requestEvent.respondWith(
+ new Response(undefined, {
+ status: 204,
+ }),
+ );
+ }
+ }
+}
+```
diff --git a/apps/site/tutorial/big-bot-guide/step-by-step.md b/apps/site/tutorial/big-bot-guide/step-by-step.md
new file mode 100644
index 000000000..4ec7f3ae3
--- /dev/null
+++ b/apps/site/tutorial/big-bot-guide/step-by-step.md
@@ -0,0 +1,40 @@
+---
+sidebar_position: 1
+---
+
+# Step By Step Guide
+
+THIS IS A WORK IN PROGRESS GUIDE USING THE NEW v16 OF DISCORDENO.
+
+## Understanding The Goals of This Guide
+
+This guide is a quick-paced walkthrough meant for big bot developers. It is expected that you have a decent amount of
+understanding of how to code your bots.
+
+## Is This Guide Meant For You?
+
+If your goal is not to have a bot in millions of discord servers, please find another guide/library. Discordeno is
+heavily opinionated towards optimizing for bots at scale. If you do not know what a Map or a Set is without having to
+google it, you are at the wrong place.
+
+## Why You Should Use Discordeno?
+
+The best way I can describe why you should use Discordeno, is from the words of the biggest bot developers themselves.
+After speaking to some of the developers of the biggest JS/TS bots, you begin to see a pattern of users unhappy with the
+current state of JS/TS libraries. They are no longer able to help them scale easily and are starting to move away to
+other libraries or having to make their own libraries because they need to be able to make their bot distributed.
+
+The following quotes are from developers who have bot's in atleast 1 million+ discord servers.
+
+- Flexibility like no other library.
+ - One of the big bot developers found that when their bot got too big, Eris was just very painful to optimize.
+ - "A pretty large hassle, I had to fork eris and modify it. There was a lot of interdependency on the values from
+ caches that made it difficult to remove properties "safely" without searching the entire codebase"
+ - When discovering how easy it was to do the same thing in Discordeno:
+ - "the convenience of being able to do so puts confidence in me that the lib is versatile so it'd certainly draw me
+ towards it"
+- Scalability: Standalone Gateway, Rest, Event Handler, Commands, Cache and much more.
+ - "All this sound like a dream (especially when you currently use eris)"
+
+Discordeno provides you all the tools that you need to make bot development really easy. As the old saying goes, the
+best way to learn to ride a bicycle is to actually try riding a bicycle. So let's try out Discordeno.
diff --git a/apps/site/tutorial/nodejs/CommandHandler/_category_.json b/apps/site/tutorial/nodejs/CommandHandler/_category_.json
new file mode 100644
index 000000000..dd4267393
--- /dev/null
+++ b/apps/site/tutorial/nodejs/CommandHandler/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Command Handler",
+ "position": 9
+}
diff --git a/apps/site/tutorial/nodejs/CommandHandler/command-manager.md b/apps/site/tutorial/nodejs/CommandHandler/command-manager.md
new file mode 100644
index 000000000..135a13edc
--- /dev/null
+++ b/apps/site/tutorial/nodejs/CommandHandler/command-manager.md
@@ -0,0 +1,92 @@
+---
+sidebar_position: 2
+---
+
+# Command Manager
+
+Currently, you probably have something like this in your code:
+
+```js
+const Discord = require("discordeno.js");
+// Ideally you should move to an `.env` file
+const config = require("./config.json");
+
+const bot = Discord.createBot({
+ events: {
+ messageCreate(client, message) {
+ if (message.content === "!ping") {
+ client.helpers.sendMessage(message.channelId, { content: "pong" });
+ }
+ },
+ },
+ intents: Discord.Intents.Guilds | Discord.Intents.GuildMessages,
+ token: config.token,
+});
+const client = Discord.enableCachePlugin(bot, {});
+
+Discord.startBot(client);
+```
+
+Of course, if you add more and more commands and as your code base grows, you can lose track very quickly.
+
+To avoid this, it is recommended to store the commands in separate folders divided into different categories.
+
+[Previously, we introduced you to our plugin structure, which has a lot of advantages.](../design.md)
+
+```root
+├Plugins/
+├── General/
+│ ├── commands/
+│ │ ├── ping.js
+│ │ └── ...
+├── Developer/
+│ ├── commands/
+│ │ ├── eval.js
+│ │ └── ...
+└── ...
+```
+
+**Get [this file](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/CommandManager.js) from
+the [nodejs template](https://github.com/discordeno/discordeno/tree/main/template)**
+
+```js
+const CommandManager = require("./template/Managers/CommandManager.js");
+const manager = new CommandManager({});
+manager.load({ plugin: true }); // Load the commands
+client.commands = manager;
+
+client.commands.cache.get("ping"); // Get the `ping` command
+```
+
+The Manager will automatically iterate through all files in the folder and then load them into the cache property, which
+is mapped on the command name.
+
+**Take a look at [Create Command](./create-command.md) to learn how to create a command.**
+
+## Handle Command
+
+The manager also contains a handler for executing the command when a message is received.
+
+:::important
+
+Currently checks for permissions, cooldowns, and rate limits are not covered, but these will be added soon.
+
+:::
+
+### Message Create Event:
+
+```js
+module.exports = async (client, message) => {
+ client.commands.isCommand(message);
+};
+```
+
+### Interaction Create Event:
+
+```js
+module.exports = async (client, interaction) => {
+ client.commands.isInteraction(interaction);
+};
+```
+
+You can also customize the `isCommand` function to your use case.
diff --git a/apps/site/tutorial/nodejs/CommandHandler/create-command.md b/apps/site/tutorial/nodejs/CommandHandler/create-command.md
new file mode 100644
index 000000000..806d71c61
--- /dev/null
+++ b/apps/site/tutorial/nodejs/CommandHandler/create-command.md
@@ -0,0 +1,61 @@
+---
+sidebar_position: 3
+---
+
+# Create Command
+
+One of the most important features we wanted in our template, was that you can use the same code for handling
+`slash commands` and `message based commands`.
+
+This can be done by saving the static class in the command cache, creating a constructor and passing the desired data.
+Moreover the `BaseCommand` is extended with the `Response Command` class, so you can take advantage of functions such as
+`.reply()`
+
+**Copy the [`BaseCommand`](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures/BaseCommand.js)
+&
+[`CommandResponses`](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures/CommandResponses.js)
+code from the template**
+
+### Creating a Ping Command:
+
+```js
+const BaseCommand = require("../../../Structures/BaseCommand.js");
+const Embed = require("../../../Structures/Embed.js");
+
+class pingCommand extends BaseCommand {
+ static name = "ping";
+ static description = "See if the bot latency is okay";
+ static usage = "";
+ static category = "General";
+ static slash = { name: "ping", category: "info" };
+
+ constructor(data) {
+ super(data);
+ }
+
+ async execute() {
+ const msg = await this.reply({content: `Pinging...`});
+ // Assign properties to the response
+ const ping = msg.timestamp - this.message.timestamp;
+
+ const embed = new Embed()
+ .setTitle(`The Bots ping is ${ping} ms`)
+ .toJSON();
+
+ // Edit Message with the Embed
+ return await msg.edit({embeds: [embed] });
+ });
+ }
+}
+
+module.exports = pingCommand;
+```
+
+- The `BaseCommand` is extended with the `CommandResponses` class.
+- The ping command class is extended with the `BaseCommand` class.
+- Some static properties are assigned to the ping command class, in order to access it in the cache, such as `name`,
+ `description`, `usage`, `category` and `slash`...
+- The `execute()` function will be called, when the command has been run by the user.
+- The constructor allows to access data, such as `this.message`, `this.args`, `this.client`...
+
+You can customize the `CommandManager` file, in order to pass arguments in the `execute()` function.
diff --git a/apps/site/tutorial/nodejs/CommandHandler/getting-started.md b/apps/site/tutorial/nodejs/CommandHandler/getting-started.md
new file mode 100644
index 000000000..622175240
--- /dev/null
+++ b/apps/site/tutorial/nodejs/CommandHandler/getting-started.md
@@ -0,0 +1,22 @@
+---
+sidebar_position: 1
+---
+
+# Getting Started with the Command Manager
+
+One of the most important characteristics of bots is that they have commands that can be used to interact with the bot.
+
+Hard coding your commands in an event function is not the best code practice and should be strictly prevented.
+
+In the following we will show you how to create a command manager, which is compatible with Discordeno's Client.
+
+- Load Commands
+- Handle Commands
+- Reload Commands
+
+:::info template
+
+You can also copy the
+[`CommandManager` from the template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/CommandManager.js).
+
+:::
diff --git a/apps/site/tutorial/nodejs/EventHandler/_category_.json b/apps/site/tutorial/nodejs/EventHandler/_category_.json
new file mode 100644
index 000000000..8dc1b47c3
--- /dev/null
+++ b/apps/site/tutorial/nodejs/EventHandler/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Event Handler",
+ "position": 8
+}
diff --git a/apps/site/tutorial/nodejs/EventHandler/event-manager.md b/apps/site/tutorial/nodejs/EventHandler/event-manager.md
new file mode 100644
index 000000000..bb95281ee
--- /dev/null
+++ b/apps/site/tutorial/nodejs/EventHandler/event-manager.md
@@ -0,0 +1,119 @@
+---
+sidebar_position: 2
+---
+
+# Create Event Manager
+
+In order to process certain events, you must provide the Discordeno client with functions for these events.
+
+```js
+const Discord = require("discordeno");
+const config = require("./config.json");
+
+const client = Discord.createBot({
+ events: {
+ ready(client, payload) {
+ console.log(`Successfully connected Shard ${payload.shardId} to the gateway`);
+ },
+
+ async messageCreate(client, message) {
+ if (message.content === "!ping") {
+ await client.helpers.sendMessage(message.channelId, { content: "pong" });
+ }
+
+ console.log(`Received message: ${message.content || message.embeds}`);
+ },
+ },
+ intents: ["Guilds", "GuildMessages"],
+ token: config.token,
+});
+
+Discord.startBot(client);
+```
+
+As you listen to more and more events, the functions code grows along with them, so you can quickly lose track.
+
+To avoid this, we recommend storing the event functions divided into files in a separate folder.
+
+## Create Event Folder
+
+Create a folder called `events` in your project folder.
+
+:::info note
+
+The event files have to be named using camelCase so that they can be understood by the client. e.g `message` ->
+`messageCreate.js`. You can check the typings see how the events are called.
+
+:::
+
+Ready Event:
+
+```js
+module.exports = (client, payload) => {
+ if (payload.shardId + 1 === client.gateway.maxShards) {
+ // All Shards are ready
+ console.log(`Successfully connected to the gateway as ${payload.user.username}#${payload.user.discriminator}`);
+ }
+};
+```
+
+## Load your Events
+
+```js
+const fs = require("fs");
+const path = require("path");
+
+const resolveFolder = (folderName) => path.resolve(__dirname, ".", folderName);
+
+class EventManager {
+ constructor(client) {
+ this.cache = new Map();
+ this._events = {};
+ }
+
+ load(options = {}) {
+ const eventsFolder = resolveFolder("../events");
+ fs.readdirSync(eventsFolder).map(async (file) => {
+ if (!file.endsWith(".js")) return;
+
+ const fileName = path.join(eventsFolder, file);
+ const event = require(fileName);
+ const eventName = file.split(".")[0];
+
+ this._events[`${eventName}`] = event;
+ });
+
+ return this._events;
+ }
+}
+
+module.exports = EventManager;
+```
+
+The code above, which can also be found in the
+[template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/EventManager.js) will loop
+through all the files in the `events` folder and load the functions into the `_events` object.
+
+In order to let the client know which events should be processed, you need to pass the functions in the
+`createBot.events` object.
+
+```js
+const Discord = require("discordeno");
+const config = require("./config.json");
+
+const EventManager = require("./Managers/EventManager.js");
+const events = new EventManager({});
+
+const client = Discord.createBot({
+ events: events.load({}),
+ intents: ["Guilds", "GuildMessages"],
+ token: config.token,
+});
+
+Discord.startBot(client);
+```
+
+Moreover, you can customize the `EventManager` and add more functionality to it and make it exactly fit your needs or
+even emit events, by extending it.
+
+Of course you wonder what you can do with all of this now. We will explain this further on the next page.
diff --git a/apps/site/tutorial/nodejs/EventHandler/getting-started.md b/apps/site/tutorial/nodejs/EventHandler/getting-started.md
new file mode 100644
index 000000000..350c2e655
--- /dev/null
+++ b/apps/site/tutorial/nodejs/EventHandler/getting-started.md
@@ -0,0 +1,29 @@
+---
+sidebar_position: 1
+---
+
+# Getting Started with the Event Handler
+
+An event handler is essential to process the data, which Discord sends to you.
+
+With a good implementation, you will have a nice code structure and thus have a good overview in long term.
+
+Since the `EventEmitter` class is commonly used you probably already know it from other libraries.
+
+Discordeno decided against it as it comes with several downsides which are mentioned below.
+
+- It's easy to create memory leaks, when you add too many listeners or go carelessly with it.
+- Many fragmented parts of event code complicate maintenance.
+- ErrorHandling is difficult and debugging is harder when many listeners are open for the same events.
+
+Performance plays a more important role than handling, however this event management system can be easily implemented
+since it only needs a few changes in your code.
+
+In the following we will show you, how to create an event manager, which is compatible with Discordeno's Client.
+
+:::info template
+
+You can also copy the
+[`EventManager` from the template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/EventManager.js).
+
+:::
diff --git a/apps/site/tutorial/nodejs/EventHandler/handle-event.md b/apps/site/tutorial/nodejs/EventHandler/handle-event.md
new file mode 100644
index 000000000..2429f7f1d
--- /dev/null
+++ b/apps/site/tutorial/nodejs/EventHandler/handle-event.md
@@ -0,0 +1,75 @@
+---
+sidebar_position: 3
+---
+
+# Handle Events
+
+When an event is fired, Discordeno sends two important things: the `client` instance and the `payload`.
+
+As mentioned in the `Structure` section, the `payload` object does not contain any functions as it's a plain json
+object.
+
+In order to take use of our nice built structures, we need to transform the payload into a structure.
+
+:::info
+
+The Structures can be found [here](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures)
+
+:::
+
+Sometimes it's important to listen to events, in order to get informed of changes and updating the cache based on it.
+
+### Message Event
+
+This file should be called `messageCreate.js`.
+
+```js
+const Message = require("./structures/Message");
+
+module.exports = async (client, payload) => {
+ const message = client.messages.forge(payload);
+
+ if (message.author.bot) return;
+ if (message.content === "!ping") return await message.reply("pong");
+};
+```
+
+### Interaction Event
+
+This file should be called `interactionCreate.js`.
+
+```js
+const Interaction = require("./structures/Interaction");
+
+module.exports = async (client, payload) => {
+ const interaction = client.interactions.forge(payload);
+
+ if (interaction.data.name === "ping") return await interaction.reply({ content: "pong" });
+};
+```
+
+### Ready Event
+
+This file should be called `ready.js`.
+
+:::tip
+
+There is a small difference with the `ready` Event. The Event is fired `shard` wise, in other words it fires every time
+a `shard` becomes ready.
+
+:::
+
+In order to fire the "real event" a small code snippet has to be added to the `ready` Event.
+
+```js
+const User = require("../Structures/User");
+
+module.exports = async (client, payload) => {
+ client.user = client.users.forge(payload.user);
+
+ if (payload.shardId + 1 === client.gateway.maxShards) {
+ // All Shards are ready
+ console.log(`Successfully connected to the gateway as ${client.user.tag}`);
+ }
+};
+```
diff --git a/apps/site/tutorial/nodejs/Structures/_category_.json b/apps/site/tutorial/nodejs/Structures/_category_.json
new file mode 100644
index 000000000..b7a1b76c1
--- /dev/null
+++ b/apps/site/tutorial/nodejs/Structures/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Structures",
+ "position": 7
+}
diff --git a/apps/site/tutorial/nodejs/Structures/collectors.md b/apps/site/tutorial/nodejs/Structures/collectors.md
new file mode 100644
index 000000000..f999a8957
--- /dev/null
+++ b/apps/site/tutorial/nodejs/Structures/collectors.md
@@ -0,0 +1,55 @@
+---
+sidebar_position: 5
+---
+
+# Create Collectors
+
+Some of your commands or features are sometimes based on user interactions. E.g. if a user presses a button and you want
+to know whether it was pressed. This is actually done by listening to the `interactionCreate` event.
+
+But sometimes you need to access locale variables or don't want to "hardcode" the part.
+
+That's why it's sometimes recommended to create collectors.
+
+Collectors are listeners that listen to a specific event. In addition, you can provide a filter, so you only receive
+certain interactions.
+
+## Use a Collector
+
+:::note Template The template code is used below. You must have the EventManager part to use the collector feature. :::
+
+We have a pre-made class for collectors which you can find
+[here](https://github.com/meister03/discordeno.js/blob/master/Util/Collectors.js).
+
+```js
+const Discord = require("discordeno.js");
+const filter = (m) => m.data?.customId === "warn_modal" && m.user.id === interaction.user.id;
+const listener = client.eventListener; // When the eventListener property is named different
+const collector = new Discord.Collector("interactionCreate", {
+ client: client,
+ timeout: 60000,
+ filter,
+ max: 20,
+ listener,
+});
+collector.on("collect", (m) => {
+ const interaction = client.interactions.forge(m);
+ // Stop Collector
+ // collector.stop();
+});
+
+// Fires on a timeout, when the collector has reached the max amount of interactions or when it has been closed
+collector.on("end", (collected) => {
+ // Map of Collected Interactions
+ console.log(collected);
+});
+```
+
+As you can see, this opens up many possibilities. You can listen to any event and get the interaction you need.
+
+### Collector Options
+
+`filter`: Function, just fire the event if the filter returns true. `timeout`: Number, the time in milliseconds until
+the collector times out. `max`: Number, the max amount of interactions the collector can collect. `listener`: Function,
+the listener that will be fired when the collector collects an interaction. Just required when client property is named
+differently.
diff --git a/apps/site/tutorial/nodejs/Structures/components.md b/apps/site/tutorial/nodejs/Structures/components.md
new file mode 100644
index 000000000..123d52b70
--- /dev/null
+++ b/apps/site/tutorial/nodejs/Structures/components.md
@@ -0,0 +1,223 @@
+---
+sidebar_position: 4
+---
+
+# Create Components
+
+Since Discord has decided to make message content accessible only to privileged bots, components will play an
+increasingly important role in the future. Discord has released some components already and many more will follow. Of
+course, this opens up completely new possibilities. On the one hand, it improves the user experience and on the other
+hand, the interactions can be easily handled by the developer.
+
+To take advantage of this, we'll go into more detail on how to use them.
+
+:::note Runtime Overhead
+
+Constructor classes are nice to use and make your code look better, but they incur a slight runtime overhead compared to
+just using raw data because they still execute methods, which takes more time to process.
+
+:::
+
+We already have a Template for `Components`, which can be found
+[here](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js).
+
+## Different Components:
+
+There are many different components, which you can quickly read about here:
+
+### Action Row (`type: 1`):
+
+This is a top level component, which contains a limited amount of other components. It can be described as container.
+
+An Action Row ...
+
+- can not include an action row
+- can maximal have 5 Buttons
+- can have 1 SelectMenu
+- can have 1 Text Input (only available in modal responses)
+
+### Button (`type: 2`):
+
+Buttons are interactive components, are bound to a message and they sent an interaction payload, when a user clicks on
+it.
+
+
+
+- Needs a customId, except the Link Button
+- An Action Row can have maximal 5 Buttons
+
+There are different styles of buttons, which can be used:
+
+- `1` - PRIMARY - blurple - customId required
+- `2` - DEFAULT - grey - customId required
+- `3` - SUCCESS - green - customId required
+- `4` - DANGER - red - customId required
+- `5` - LINK - grey - url required
+
+### Select Menu (`type: 3`):
+
+Select Menus are a simple drop-down with selectable options. They accept a set of allowed selects, which sends an
+interaction payload, when a user selects sth. from the menu.
+
+
+
+- You can specify a range of allowed selects (`minValue` and `maxValue`)
+- Every Select Item can have an `emoji` and has a `value`, in order to identify the selected item
+- A default Select Item can be set
+- An Action Row can have maximal 1 Select Menu
+
+### Text Input (`type: 4`):
+
+Text Inputs are interactive components, which can just be sent with a modal response.
+
+- You can specify a range of text length (`minLength` and `maxLength`)
+- You can add a placeholder, a pre-filled value and specify whether the text input is required
+- An Action Row can have maximal 1 Text Input
+
+## Send Components
+
+As mentioned above there are different types of components. This requires to define a type, so that Discord knows, which
+component you want to use.
+
+```js
+class ActionRow {
+ constructor(options = {}) {
+ this.type = 1;
+ }
+
+ setComponents(...components) {
+ this.components = components;
+ return this;
+ }
+}
+```
+
+```js
+const button = new Button();
+const button2 = new Button();
+const actionRow = new ActionRow().setComponents(button, button2);
+```
+
+This code will obviously not work because it's a missing a lot required of data. The other reason is that we can't send
+a class to Discord, we need sth. to transform it to a json object.
+
+We have a pre-made class for components which you can find
+[here](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js).
+
+### Button
+
+```js
+const Discord = require("discordeno.js");
+const message = client.messages.forge(rawMessage);
+
+const button = new Discord.Component()
+ .setType("BUTTON")
+ .setStyle("LINK")
+ .setLabel("Click me!")
+ .setUrl("https://google.com")
+ .toJSON();
+
+// Button with raw types
+const button2 = new Discord.Component()
+ .setType(2)
+ .setStyle(4)
+ .setLabel("DO NOT CLICK")
+ .setCustomId("12345")
+ .toJSON();
+
+const actionRow = new Discord.Component()
+ .setType("ACTION_ROW")
+ .setComponents(button, button2)
+ .toJSON();
+
+// Message to send
+const messageOptions = { content: "hello", components: [actionRow] };
+
+// await client.helpers.sendMessage(channelId, messageOptions); // Do it the raw way
+message.channel.send(messageOptions); // Do it with the structure
+```
+
+As you can see, for simplicity you can use strings instead of numbers (types), which are hard to remember.
+
+### Select Menu
+
+```js
+const Discord = require("discordeno.js");
+const message = client.messages.forge(rawMessage);
+
+const selectMenu = new Discord.Component()
+ .setType("SELECT_MENU")
+ .setCustomId("12345")
+ .setOptions([
+ {
+ label: "Option 1",
+ value: "1",
+ description: `This is option 1`,
+ },
+ {
+ label: "Option 2",
+ value: "2",
+ description: `This is option 2`,
+ },
+ {
+ label: "Default Option",
+ value: "3",
+ description: `Default option...`,
+ default: true,
+ },
+ ])
+ .setPlaceholder("Select an option")
+ .toJSON();
+
+const actionRow = new Discord.Component()
+ .setType("ACTION_ROW")
+ .setComponents(selectMenu)
+ .toJSON();
+
+const messageOptions = { content: "hello", components: [actionRow] };
+
+// await client.helpers.sendMessage(channelId, messageOptions); // Do it the raw way
+message.channel.send(messageOptions); // Do it with the structure
+```
+
+### Text Input
+
+```js
+const Discord = require("discordeno.js");
+const interaction = client.messages.forge(rawInteraction);
+
+const textInput = new Component()
+ .setType("TEXT_INPUT")
+ .setStyle("SHORT")
+ .setCustomId("t1")
+ .setLabel("User ID")
+ .setPlaceholder("User ID")
+ .setRequired(true)
+ .setMaxLength(20)
+ .setMinLength(1)
+ .toJSON();
+
+const textInput2 = new Component()
+ .setType("TEXT_INPUT")
+ .setStyle("PARAGRAPH")
+ .setCustomId("t2")
+ .setLabel("Reason")
+ .setPlaceholder("Reason for Ban")
+ .setRequired(false)
+ .setMaxLength(300)
+ .toJSON();
+
+const actionRow = new Component().setType("ACTION_ROW").setComponents(textInput).toJSON();
+const actionRow2 = new Component().setType("ACTION_ROW").setComponents(textInput2).toJSON();
+
+interaction.popupModal({
+ customId: "ban_modal",
+ title: "Ban User",
+ components: [actionRow, actionRow2],
+});
+```
+
+### Receive Interactions
+
+When a user clicks a button or selects an option from a Select Menu, Discord sends an `interactionCreate` event, which
+contains the information necessary to process it.
diff --git a/apps/site/tutorial/nodejs/Structures/create-structure.md b/apps/site/tutorial/nodejs/Structures/create-structure.md
new file mode 100644
index 000000000..3be34dd12
--- /dev/null
+++ b/apps/site/tutorial/nodejs/Structures/create-structure.md
@@ -0,0 +1,100 @@
+---
+sidebar_position: 2
+---
+
+# Create Structure
+
+Structures are often used to transform data and add methods to existing objects. To make it easier to work with them.
+
+Imagine you have a channel object to which you want to send a message.
+
+```js
+const data = {
+ id: 806947972004839444n,
+ name: "spam-and-bots",
+};
+```
+
+The recommended way would be:
+
+```js
+await client.helpers.sendMessage(data.id, { content: "hello" });
+```
+
+However, you probably want to use something shorter, such as the following:
+
+```js
+class Channel {
+ constructor(client, data) {
+ this.client = client;
+ this.id = data.id;
+ this.name = data.name;
+ }
+
+ async send(options) {
+ return await this.client.helpers.sendMessage(this.id, options);
+ }
+}
+```
+
+Now you can use the `.send()` method on the channel object without using such a long code:
+
+```js
+const channel = new Channel(client, data);
+await channel.send({ content: "hello" });
+```
+
+Moreover, you can modify the `.send()` method to better suit your use case e.g not send the message if the channel is
+blacklisted.
+
+This naturally opens a lot of opportunities and makes coding a lot easier. Because you decide what you want to do with
+the data, how the methods are named and how you want to process the request.
+
+## Using Template Structures:
+
+When you are migrating from another library and you want to utilize the djs-like wrapper, you'll likely choose to
+continue using special structures. Therefore we have ready-made structures for the wrapper `Discordeno.js`.
+
+- [Guild](https://github.com/meister03/discordeno.js/tree/master/Structures/Guild.js)
+- [Channel](https://github.com/meister03/discordeno.js/tree/master/Structures/Channel.js)
+- [Role](https://github.com/meister03/discordeno.js/tree/master/Structures/Role.js)
+- [Member](https://github.com/meister03/discordeno.js/tree/master/Structures/Member.js)
+- [User](https://github.com/meister03/discordeno.js/tree/master/Structures/User.js)
+- [Message](https://github.com/meister03/discordeno.js/tree/master/Structures/Message.js)
+- [Interaction](https://github.com/meister03/discordeno.js/tree/master/Structures/Interaction.js)
+- [Emoji](https://github.com/meister03/discordeno.js/tree/master/Structures/Emoji.js)
+- [Webhook](https://github.com/meister03/discordeno.js/tree/master/Structures/Webhook.js)
+- [Embed](https://github.com/meister03/discordeno.js/tree/master/Structures/Embed.js)
+- [Component](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js)
+- [Collection](https://github.com/meister03/discordeno.js/tree/master/Structures/Collection.js)
+
+We recommend that you check the wrappers [Readme](https://github.com/meister03/discordeno.js#discordclient) in order to
+construct the client for following the Guide
+
+**Using the Structures:**
+
+```js
+const Discord = require("discordeno.js");
+const client = new Discord.Client(clientOptions, cacheOptions); //See the Readme above
+Discord.startBot(client);
+const guild = client.guilds.forge(guildData);
+const channel = guild.channels.forge(channelData);
+const role = guild.roles.forge(roleData);
+const member = guild.members.forge(memberData);
+const user = guild.users.forge(userData);
+const message = guild.messages.forge(messageData);
+const interaction = guild.interactions.forge(interactionData);
+const emoji = guild.emojis.forge(emojiData);
+
+const webhook = new Discord.Webhook(client, webhookData);
+const embed = new Discord.Embed(embedData); // embedData is optional
+const component = new Discord.Component(componentData); // componentData is optional
+const collection = new Discord.Collection();
+```
+
+Some popular methods have been added to the structures so that you can use them without having to come up with your own.
+In order to use the Structures from the Wrapper, you need to invoke the `.forge` method with the raw discord data,
+whereas it will construct the structure for you.
+
+Next we're going to give a better insight into how create [`Embeds`](embeds) and [`Components`](components) with the
+wrappers structures.
diff --git a/apps/site/tutorial/nodejs/Structures/embeds.md b/apps/site/tutorial/nodejs/Structures/embeds.md
new file mode 100644
index 000000000..9c2864e16
--- /dev/null
+++ b/apps/site/tutorial/nodejs/Structures/embeds.md
@@ -0,0 +1,106 @@
+---
+sidebar_position: 3
+---
+
+# Create Embeds
+
+Embeds are widely used by bots in order to display messages in a fancy way.
+
+Unfortunately, the Discord API does not accept funky classes such as `new MessageEmbed().setTitle("hello")`, instead it
+takes a json object, e.g. `{ title: "hello" }`. Therefore, we need to create an embed Structure that converts the
+user-supplied data into the format which Discord uses.
+
+:::note Runtime Overhead
+
+Constructor classes are nice to use and make your code look better, but they incur a slight runtime overhead compared to
+just using raw data because they still execute methods, which takes more time to process.
+
+:::
+
+```js
+class Embed() {
+ constructor() {}
+
+ setTitle(title) {
+ this.title = title;
+ }
+}
+```
+
+Now we have created a class which we can use to create embeds. But we can't just send this to Discord.
+
+So we need an additional method which will convert the data from the class to the correct format.
+
+```js
+class Embed(){
+ constructor() {}
+
+ setTitle(title) {
+ this.title = title;
+ }
+
+ toJSON() {
+ return {
+ title: this.title
+ }
+ }
+}
+```
+
+Wow, now you can create a embed and send it to Discord.
+
+```js
+const Channel = require("./structures/Channel"); // Path to structure
+
+const channel = new Channel(client, data);
+await channel.send({ embeds: [embed] });
+```
+
+You probably want more methods which you can use to create embeds.
+[Here is how the Embed Structure looks like](https://github.com/meister03/discordeno.js/blob/master/Structures/Embed.js)
+
+### Using the Embed Structure:
+
+```js
+const Discord = require("discordeno.js");
+
+const channel = client.channels.forge(channelData);
+const showCaseEmbed = new Discord.Embed()
+ .setColor(0x00AE86)
+ .setTitle("A Random Title")
+ .setURL("https://github.com/discordeno")
+ .setAuthor({
+ name: "Author name",
+ iconUrl: "https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png",
+ url: "https://github.com/discordeno",
+ })
+ .setDescription("A Random Description")
+ .setThumbnail("https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png")
+ .addFields(
+ { name: "Field 1 Name", value: "Normal Field Value" },
+ { name: "\u200B", value: "\u200B" },
+ { name: "Field 2 Name", value: "Inline Field Value", inline: true },
+ { name: "Field 3 Name", value: "Inline Field Value", inline: true },
+ )
+ .addField({ name: "Field 4", value: "Field Value" })
+ .setImage("https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png")
+ .setTimestamp()
+ .setFooter({
+ text: "A Footer Text",
+ iconUrl: "https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png",
+ })
+ .toJSON();
+
+await channel.send({ embeds: [showCaseEmbed] });
+```
+
+### Embed Limits:
+
+- Title: 256 characters
+- Description: 4096 characters
+- Field Name: 256 characters
+- Field Value: 1024 characters
+- Footer Text: 2048 characters
+- Author Name: 256 characters
+- 10 Embeds per message
+- In total over all 10 Embeds not more than 6000 characters
diff --git a/apps/site/tutorial/nodejs/Structures/getting-started.md b/apps/site/tutorial/nodejs/Structures/getting-started.md
new file mode 100644
index 000000000..1898e757b
--- /dev/null
+++ b/apps/site/tutorial/nodejs/Structures/getting-started.md
@@ -0,0 +1,30 @@
+---
+sidebar_position: 1
+---
+
+# Getting Started with Structures
+
+As previously mentioned, Discordeno was built with as few classes as possible, this is in favor of performance.
+
+For example, you cannot execute functions on objects.
+
+```diff
+- message.channel.send({content: "hello"})
++ client.helpers.sendMessage(message.channel.id, {content: "hello"})
+```
+
+This seems to be more complicated at first, but has many advantages:
+
+- You get full control over the actions
+- Errors are easier to debug
+- A validation by classes does not have to take place
+
+One of the disadvantages is that you have to change a lot in your code.
+
+Of course, we recommend that you try out the upper way, but we will introduce structures in this guide because they are
+used by many users who eventually want to migrate.
+
+For example, if you want to get correctly formatted objects, structures are obviously beneficial, because they support
+the readability of the code by their ease of use
+
+In the following, we will introduce how to create your own structures and how to use the ones available in the template.
diff --git a/apps/site/tutorial/nodejs/_category_.json b/apps/site/tutorial/nodejs/_category_.json
new file mode 100644
index 000000000..b45c7e9ac
--- /dev/null
+++ b/apps/site/tutorial/nodejs/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Nodejs",
+ "position": 3
+}
diff --git a/apps/site/tutorial/nodejs/create-application.md b/apps/site/tutorial/nodejs/create-application.md
new file mode 100644
index 000000000..b7197a8ba
--- /dev/null
+++ b/apps/site/tutorial/nodejs/create-application.md
@@ -0,0 +1,29 @@
+---
+sidebar_position: 3
+---
+
+# Create Application
+
+1. Go to the [Developer Portal](https://discord.com/developers/applications) and create a new application.
+2. Navigate to the Section `Bot` and confirm with "Yes, do it!"
+3. Now copy your token and save it under a safe environment.
+
+:::caution Token Security
+
+Keep your token safe, because it is like a password that grants access to your bot, which then can be used for mass
+DMing, mass banning or any other kind of malicious activity.
+
+:::
+
+## Add your Bot to your Server
+
+In order to use your Bot, it should be in a server where you can interact with it.
+
+1. Go to the [Developer Portal](https://discord.com/developers/applications) and click on your previously created bot.
+2. Click on `OAuth2` and there go to the `URL Generator`.
+3. Select the `bot` and the `applications.commands` scope.
+4. Scroll down and select the `Administrator` permission.
+5. Copy the generated URL and open it in your browser.
+6. Select your Server and click the invite button.
+
+The bot should now have been added to your server and show as an offline user.
diff --git a/apps/site/tutorial/nodejs/design.md b/apps/site/tutorial/nodejs/design.md
new file mode 100644
index 000000000..580e70c7b
--- /dev/null
+++ b/apps/site/tutorial/nodejs/design.md
@@ -0,0 +1,203 @@
+---
+sidebar_position: 6
+---
+
+# Design
+
+In order to ensure long-term scalability and maintainability, the code structure is of enormous importance. In the
+following, we show how such a code structure could look like.
+
+The essential parts are a `CommandHandler/CommandManager`, `EventHandler/EventManager`, lots of `Structures` in order to
+code faster and `Plugins`, where your different features will be, such as `Commands`, `DB Stuff`...
+
+## Code Structure
+
+We recommend following structure for your code:
+
+```root
+├index.js
+├─Structures/
+├─Managers/
+├─events/
+├─Plugins/
+├── General/
+│ ├── commands/
+│ │ ├── ping.js
+│ │ └── ...
+├── Developer/
+│ ├── commands/
+│ │ ├── eval.js
+│ │ └── ...
+├─Util/
+└── ...
+```
+
+The following explains why this structure is suitable. If you want to follow this guide further, you should create these
+folders.
+
+In the `Managers` folder the Managers will be added e.g. `CommandManager.js`, `EventManager.js`. Generally codes, which
+manage the system.
+
+While in the `Structures` folder mainly classes are added like `BaseCommand.js`, `CommandResponse.js`, `Embed.js`,
+`Components.js`, which make it easier to add methods to objects.
+
+The `events` folder will contain the event handlers such as `messageCreate.js`, `debug.js`
+
+Your many useful features and categories end up in the `Plugins` folder, where they should be categorically divided into
+many folders.
+
+The `Util` folder contains functions or classes that help you convert certain things, such as timestamps, into a
+human-readable format.
+
+## CommandHandler & BaseCommand
+
+The `CommandHandler` is the main class of the bot, which will handle all the commands and the events received from
+Discord.
+
+The `BaseCommand` is the base class of all commands, which will be extended with the`CommandResponse` class.
+
+### Steps showed in the following Guide
+
+- Loading commands from different plugins
+- Deploying slash commands
+- Handling `messageCreate` & `interactionCreate` events
+- Command rate limit handling
+- Handle `Interaction` & `Message` commands with the same code
+- Validating user provided arguments
+- Correct permission and error handling
+- Hot reloading commands
+- Creating message and interaction collectors
+
+## EventHandler
+
+You probably realized that Discordeno does not use an `EventEmitter` to fire the events, but your own event function is
+fired.
+
+There are ways to adapt to an `EventEmitter`, but we decided against it for the following reasons:
+
+- It's easy to create memory leaks, when you add too many listeners or go carelessly with it.
+- Many fragmented parts of event code complicate maintenance.
+- ErrorHandling is difficult and debugging is harder when many listeners are open for the same events.
+
+## Structures
+
+Structures are essential to abstract larger parts of code in smaller ready-made methods and to modify them if necessary.
+
+Example:
+
+```js
+class Command {
+ static name = "ping";
+ static aliases = ["pong"];
+ static botPermission = ["SEND_EMBED_LINKS"];
+
+ run(message, args) {
+ // do something
+ }
+}
+```
+
+It would be annoying adding everytime the `botPermission` property to the class Command, when the Permission is used
+from every Command, then it is unnecessary to add it, when you can extend the class.
+
+It would be annoying to add the `botPermission` property to the command class every time the same permissions are used
+by each command. Extending the class makes this extra step obsolete.
+
+```js
+class BaseCommand {
+ constructor(client) {
+ this.client = client;
+ this.basePermission = ["SEND_EMBED_LINKS"];
+ }
+}
+
+class Command extends BaseCommand {
+ static name = "ping";
+ static aliases = ["pong"];
+
+ constructor(data) {
+ super(data);
+ }
+
+ run(message, args) {
+ // do something
+ }
+}
+```
+
+## Plugins
+
+The plugins folder helps you categorize your code into many parts to give some structure.
+
+Of course, this has many advantages, you have a much clearer code, you can debug problems much easier.
+
+This also opens possibilities for open source contributions, since not all parts of the code have to be published in
+order to add new plugins, since they are "independent".
+
+There will be the main `Plugins` folder, which by default contains a `General` folder for all your base commands. The
+`Plugins` folder will also contain all your other plugins.
+
+## Error Handling
+
+One of the most important things is how to handle errors. This is done to provide a user-friendly experience and to find
+errors faster.
+
+You should catch errors and log them in your logger so you can fix them later. There are several open source `Sentry`'s
+that give you a good overview of the latest errors through a website.
+
+Sometimes errors have a positive effect on maintainability and scalability.
+
+In addition, handling errors caused by users is very important to increase transparency. If they don't know why the
+error happened, then they'll be very surprised with what they did wrong and might even remove your bot from their
+server.
+
+## Caching
+
+Normally libraries cache all the info they get, which can of course be helpful at the beginning to discover all
+functionalities but later it turns out to be a resource-consuming method. Therefore, this way should be avoided.
+
+Discordeno allows `Custom Caching` and even `Custom Property Caching` which gives you fine-grained control over the
+caching of data. Normally you only need 20% of the data received by Discord, which makes caching unnecessary in most
+cases.
+
+There are also some `Filter` and `Sweeper` methods which help you to empty unused cache values.
+
+## Cross Communication & Scaling
+
+If you are running many different processes, such as a Welcomer API, communication is of central importance in order to
+send or receive data, with which you can then perform certain actions.
+
+Cross communication can be easily done with sockets or a TCP client.
+
+This brings up this Structure:
+
+```js
+Bridge (Heart)
+- Machine 1
+ - Cluster [0-9]
+- Machine 2
+ - Cluster [10-18]
+- Machine 3 -> Welcomer Api
+- Machine 4 -> Dashboard
+```
+
+It's important to use something fast to have a proper "real time" communication.
+
+Discordeno already offers many internal options for scaling bots, no matter what size.
+
+As you scale, you will likely separate many parts of your bot and put them in separate processes, such as a
+`RestManager`, a `Gateway Manager` etc.
+
+This of course opens up a lot of possibilities:
+
+- Zero downtime updates
+- Global cache
+- Synced rate limits
+
+[Check the Github Readme for more information](https://github.com/discordeno/discordeno#features)
+
+:::tip congratulations
+
+You just learned how to design a scalable bot, let's get into implementing it with the next pages.
+
+:::
diff --git a/apps/site/tutorial/nodejs/getting-started.md b/apps/site/tutorial/nodejs/getting-started.md
new file mode 100644
index 000000000..04a65d5d3
--- /dev/null
+++ b/apps/site/tutorial/nodejs/getting-started.md
@@ -0,0 +1,48 @@
+---
+sidebar_position: 1
+---
+
+# Getting Started
+
+If you are reading this, you probably want to create a Discord bot with Discordeno or migrate from popular libraries like Discord.js.
+
+If this is going to be your first time making a bot, you should use Deno instead of Node.js. Although in some cases, Deno might not be suitable for you because of missing packages or a code base that is too large to migrate to a slightly different language.
+
+This guide will help you make your first Discord bot using Node.js or even migrate your bot from another library.
+
+Moreover this guide will utilize two different options. One option to use the Discordeno package without any frameworks
+and one, which uses the wrapper called `Discordeno.js`, which aims to achieve a djs-like interface.
+
+:::important Disclaimer
+
+Some features are not documented yet. If you want to know more about them, kindly ask for help in the
+[Discord Server](https://discord.gg/ddeno).
+
+:::
+
+## Why should I switch?
+
+Discordeno was built with the purpose of being scalable, flexible and easy to use.
+
+Libraries like `Discord.js` and `Eris` often have excessive caching behavior that can only be changed slightly without
+breaking the entire library. There is a lack of customization and many nested classes, which makes it almost impossible
+to edit the code without having unwanted side effects. Moreover scalability is only possible on a limited extend.
+
+Discordeno has been kept plain and simple, which opens up a lot of opportunities for customization such as
+`custom-caching (custom-property-caching)`, [`Standalone Rest`](../big-bot-guide/rest.md),
+[`Gateway`](../big-bot-guide/gateway.md), [`Cache`](../big-bot-guide/cache.md) and more. Check the detailed advantages
+[here](https://github.com/discordeno/discordeno#features).
+
+This guide will also help you making your code more scalable and easier to maintain with bringing you closer to the
+Discord API.
+
+# Before you start
+
+Before you start digging in this guide, you should have a solid understanding of `javascript`. If you are not familiar
+with it, then you should take a look at some popular resources.
+
+- [W3Schools Course](https://www.w3schools.com/js/DEFAULT.asp)
+- [Mozilla Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
+- [JavaScript.Info](https://javascript.info)
+
+A basic understanding is of great importance in order to solve problems skillfully.
diff --git a/apps/site/tutorial/nodejs/initial-setup.md b/apps/site/tutorial/nodejs/initial-setup.md
new file mode 100644
index 000000000..04ab56456
--- /dev/null
+++ b/apps/site/tutorial/nodejs/initial-setup.md
@@ -0,0 +1,46 @@
+---
+sidebar_position: 4
+---
+
+# Initial Setup
+
+## Config File
+
+Ideally, you should save your configs in an `.env` file. Out of simplicity for this guide, we are saving it in a
+`config.json` file.
+
+Create a file named `config.json` in your project folder and insert the following content:
+
+```json
+{
+ "token": "YOUR_TOKEN_HERE",
+ "prefix": "!"
+}
+```
+
+## Edit the main file
+
+Open the `index.js` file which you have created earlier and then insert the following content:
+
+```js
+const Discord = require("discordeno");
+const config = require("./config.json");
+
+const client = Discord.createBot({
+ events: {
+ ready(client, payload) {
+ console.log(`Successfully connected Shard ${payload.shardId} to the gateway`);
+ },
+ },
+ intents: ["Guilds", "GuildMessages"],
+ token: config.token,
+});
+
+Discord.startBot(client);
+```
+
+Now you can start your bot by running the following command in your terminal:
+
+```cli
+$ node index.js
+```
diff --git a/apps/site/tutorial/nodejs/installion.md b/apps/site/tutorial/nodejs/installion.md
new file mode 100644
index 000000000..210daf415
--- /dev/null
+++ b/apps/site/tutorial/nodejs/installion.md
@@ -0,0 +1,30 @@
+---
+sidebar_position: 2
+---
+
+# Installing Node.js and Discordeno
+
+To use the Discordeno library you first need to install Node.js and then Discordeno from NPM.
+
+Go to [nodejs.org](https://nodejs.org/en/) and download the latest version of Node.js. Open the downloaded file and
+follow the instructions of the installer to install Node.js.
+
+## Create a Folder
+
+Open your file manager and create a new folder (e.g.: `discordbot`) in your desired directory. Then open the code editor
+of your choice and create a new file (e.g.: `index.js`) in the folder you just have created.
+
+### Initalize NPM & Install Discordeno
+
+In order to keep track of the dependencies, you need to initialize NPM, which generates a `package.json` file.
+
+```cli
+$ npm init --yes
+```
+
+Then you need to install Discordeno. When you want to go along with the wrapper named `Discordeno.js`, then install it
+too. Go to your terminal and run the following command:
+
+```cli
+$ npm install discordeno
+```
diff --git a/apps/site/tutorial/nodejs/slash-command.md b/apps/site/tutorial/nodejs/slash-command.md
new file mode 100644
index 000000000..b5effda76
--- /dev/null
+++ b/apps/site/tutorial/nodejs/slash-command.md
@@ -0,0 +1,64 @@
+---
+sidebar_position: 5
+---
+
+# Slash Commands
+
+Since Discord has decided to make message content accessible only to privileged bots, message commands will play a
+subordinate role in the future. Discord users will be more used to slash commands. That's why it's essential that every
+bot offers them.
+
+In the following we will show you how to create slash commands:
+
+## Deploying Slash Commands
+
+There is a difference between global and guild commands. Global commands take a while to appear in all guilds. Guild
+commands show up directly.
+
+For this reason, we will now show how to create guild commands, in order to test them immediately.
+
+```js
+const guildId = BigInt("YOUR_GUILD_ID");
+const command = {
+ name: "ping",
+ description: "Retrieves the Bot latency",
+ options: [],
+};
+
+client.helpers.createApplicationCommand(command, guildId);
+```
+
+This is just very simple example, you can also add sub commands, select options and much more.
+
+## Handling Slash Commands
+
+Discord sends a WebSocket Event when a user runs a slash command. You can listen to this event by adding the
+`interactionCreate` function in the client.
+
+```js
+const Discord = require("discordeno");
+const config = require("./config.json");
+
+const client = Discord.createBot({
+ events: {
+ ready(client, payload) {
+ console.log(`Successfully connected Shard ${payload.shardId} to the gateway`);
+ },
+ async interactionCreate(client, interaction) {
+ if (interaction.data?.name === "ping") {
+ return await client.helpers.sendInteractionResponse(interaction.id, interaction.token, {
+ type: Discord.InteractionResponseTypes.ChannelMessageWithSource,
+ data: { content: "🏓 Pong!" },
+ });
+ }
+ },
+ },
+ intents: Discord.Intents.Guilds | Discord.Intents.GuildMessages,
+ token: config.token,
+});
+
+Discord.startBot(client);
+```
+
+The handling may see complicated in the beginning, but as mentioned before, we will introduce structures to make it
+easier.
diff --git a/apps/site/tutorialSidebars.js b/apps/site/tutorialSidebars.js
new file mode 100644
index 000000000..3fa56fd10
--- /dev/null
+++ b/apps/site/tutorialSidebars.js
@@ -0,0 +1,30 @@
+/**
+ * Creating a sidebar enables you to:
+ - create an ordered group of docs
+ - render a sidebar for each doc of that group
+ - provide next/previous navigation
+
+ The sidebars can be generated from the filesystem, or explicitly defined here.
+
+ Create as many sidebars as you want.
+ */
+
+// @ts-check
+
+/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
+const sidebars = {
+ // By default, Docusaurus generates a sidebar from the docs folder structure
+ tutorial: [{ type: "autogenerated", dirName: "." }],
+ // But you can create a sidebar manually
+ /*
+ tutorialSidebar: [
+ {
+ type: 'category',
+ label: 'Tutorial',
+ items: ['hello'],
+ },
+ ],
+ */
+};
+
+module.exports = sidebars;
diff --git a/script/generateHandlerData.ts b/script/generateHandlerData.ts
new file mode 100644
index 000000000..1df7c8227
--- /dev/null
+++ b/script/generateHandlerData.ts
@@ -0,0 +1,38 @@
+const handlers: {
+ [index: string]: any;
+} = {};
+
+const topDirs = ["./handlers"];
+
+const readDir = async (dirname: string, onContent: (content: string) => void) => {
+ const files: Deno.DirEntry[] = [];
+ for (const dirEntry of Deno.readDirSync(dirname)) {
+ files.push(dirEntry);
+ }
+ await Promise.all(
+ files.map(async (file) => {
+ if (file.isFile) {
+ const content = await Deno.readTextFile(dirname + "/" + file.name);
+ onContent(content);
+ return;
+ }
+ if (file.isDirectory) await readDir(`${dirname}/${file.name}`, onContent);
+ }),
+ );
+};
+
+await Promise.all(topDirs.map((topDir) =>
+ readDir(topDir, (content) => {
+ const handler = content.match(/export function [A-z]+\(/g);
+ if (handler === null) return;
+ const transformers = content.match(/bot.transformers.[A-z]+\(/g);
+ const event = content.match(/bot.events.[A-z]+\(/);
+
+ handlers[handler.map((handler) => handler.slice(16, -1))[0]] = {
+ transformers: transformers === null ? [] : transformers.map((result) => result.slice(4, -1)),
+ event: event === null ? undefined : event.map((result) => result.slice(4, -1))[0],
+ };
+ })
+));
+
+console.log(handlers);