diff --git a/src/content/docs/v3/guide/walkthrough/dependency-injection.mdx b/src/content/docs/v3/guide/walkthrough/dependency-injection.mdx
index d402f221d..e0976dcff 100644
--- a/src/content/docs/v3/guide/walkthrough/dependency-injection.mdx
+++ b/src/content/docs/v3/guide/walkthrough/dependency-injection.mdx
@@ -5,9 +5,6 @@ sidebar:
order: 10
---
-:::danger
-This contains version 2 code. Please view [transitioning to v3](/v3/guide/walkthrough/transition) for the `Service` API.
-:::
Since version 2.0.0, dependency injection, thanks to [iti](https://github.com/molszanski/iti), is a feature to customize your bot's utilities and structures.
@@ -127,3 +124,6 @@ import { Steps } from '@astrojs/starlight/components';
4. Now, when your bot starts, the `init` method will be called. 🎉
+
+
+
diff --git a/src/content/docs/v3/guide/walkthrough/services.mdx b/src/content/docs/v3/guide/walkthrough/services.mdx
index 660d7703a..6d5f40109 100644
--- a/src/content/docs/v3/guide/walkthrough/services.mdx
+++ b/src/content/docs/v3/guide/walkthrough/services.mdx
@@ -5,14 +5,6 @@ sidebar:
order: 8
---
-:::danger
-This is version 3 api only!!
-:::
-
-:::tip
-**TLDR:** The direct upgrade to `useContainer`. if you set up a bot with `create-bot`, check `dependencies.d.ts`.
-Dependencies are the types that Services uses.
-:::
You need some way to use dependencies in your command module. Services to the rescue!
diff --git a/src/content/docs/v4/reference/dependencies.mdx b/src/content/docs/v4/reference/dependencies.mdx
new file mode 100644
index 000000000..460938904
--- /dev/null
+++ b/src/content/docs/v4/reference/dependencies.mdx
@@ -0,0 +1,143 @@
+---
+title: Dependencies
+description: Customize & manage stateful dependencies
+sidebar:
+ order: 5
+---
+
+Manage objects which contain lots of state. If you were a previous user of Sapphire, dependency injection is the moral equivalent of `container`.
+Dependency injection promotes maintainability and helps organize imports.
+
+For example, a minimal setup for any project might look like this:
+
+import { FileTree } from '@astrojs/starlight/components';
+
+
+- src/
+ - index.js **(your main file and client)**
+ - **dependencies.d.ts** **(for intellisense)**
+
+
+
+
+```js title="src/index.js"
+const client = new Client({
+ ...options,
+});
+
+await makeDependencies((root) => {
+ root.add("@sern/client", client),
+});
+```
+
+Everything else is handled. However, you may want customize things.
+
+## Adding Dependencies to Root
+
+Each sern built dependency must implement its contracts:
+
+- `@sern/logger`: Logging data → [`Logging`](/v3/api/interfaces/logging)
+- `@sern/errors`: Handling errors and lifetime → [`ErrorHandling`](/v3/api/interfaces/errorhandling)
+- `@sern/emitter`: The key to emit events and occurences in a project → [`Emitter`](/v3/api/interfaces/emitter)
+
+## Lifecycle Hooks
+Dependencies can call a function throughout you bot's lifetime.
+
+### Init
+
+> Your object needs to initiate things. Developers are allowed to use `async` and `await`.
+
+import { Steps } from '@astrojs/starlight/components';
+
+
+1. Do you need to perform intializing behavior for a dependency?
+ ```js title="src/database.js"
+ import pg from 'pg'
+ const { Client } = pg
+ class Database {
+ __database = new Client()
+ async init() {
+ await this.__database.connect();
+ }
+ }
+ ```
+
+2. Modify your `Dependencies` interface:
+ ```js title="src/dependencies.d.ts" {4}
+ import { Database } from './services/database.js'
+ interface Dependencies extends CoreDependencies {
+ database: Database;
+ }
+ ```
+
+3. Make sure its been added:
+ ```ts title="src/index.ts" {3}
+ await makeDependencies(({ add }) => {
+ add('database', new Database())
+ })
+ ```
+
+4. Now, when your bot starts, the `init` method will be called. 🎉
+
+
+
+### Dispose
+> Your object needs to destroy things before shutdown, if a crash occurs
+
+
+
+1. Do you need to perform intializing behavior for a dependency?
+ ```js title="src/database.js"
+ import pg from 'pg'
+ const { Client } = pg
+ class Database {
+ __database = new Client()
+ async init() {
+ await this.__database.connect();
+ }
+ async dispose() {
+ console.log("Disposing database")
+ }
+ }
+ ```
+
+3. Make sure its been added:
+ ```js title="src/index.ts" {3}
+ await makeDependencies(({ add }) => {
+ add('database', new Database())
+ })
+ ```
+4. Now, when your bot starts, the `dispose` method will be called. 🎉
+
+
+:::tip
+Both `dispose` and `init` are exposed as interfaces in TypeScript. For extra typesafety it may be feasible to implement these interfaces.
+```ts
+import { type Init } from '@sern/handler'
+import pg from 'pg'
+const { Client } = pg
+class Database implements Init {
+ __database = new Client()
+ async init() {
+ await this.__database.connect();
+ }
+}
+```
+:::
+
+## Usage in Commands
+> This is for command modules, plugins only. event modules would have to use the `Service` api
+
+Your dependencies are located in SDT.
+```ts
+export default commandModule({
+ type: CommandType.Slash,
+ execute: (ctx, sdt) =>{
+ sdt.deps.database // Database
+ }
+})
+```
+
+## Service
+Service api is used for places where sern cannot inject into parameters properly. [View](../../../v3/guide/walkthrough/services/)
+
diff --git a/src/content/docs/v4/reference/getting-started.mdx b/src/content/docs/v4/reference/getting-started.mdx
index c71110d3a..2df4ec209 100644
--- a/src/content/docs/v4/reference/getting-started.mdx
+++ b/src/content/docs/v4/reference/getting-started.mdx
@@ -2,7 +2,7 @@
title: Getting Started
description: Get started with the sern framework
sidebar:
- order: 2
+ order: 1
---
import PackageManagers from '~/components/PackageManagers.astro';
diff --git a/src/content/docs/v4/reference/modules.mdx b/src/content/docs/v4/reference/modules.mdx
index b851f9d39..4ebe41cd0 100644
--- a/src/content/docs/v4/reference/modules.mdx
+++ b/src/content/docs/v4/reference/modules.mdx
@@ -2,7 +2,7 @@
title: Modules
description: Learn how to create modules for your sern bot
sidebar:
- order: 2
+ order: 3
---
@@ -10,12 +10,11 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
## Introduction
-sern operates with **modules**. At its core, modules contain `type` and `execute` fields on an object, with some code to possibly run before
-executing.
+sern operates with **modules**. At its core, modules contain `type`,`execute`, and [`plugins`](../plugins) (code ran before `execute`).
## Modules
-We'll walk you through creating your first command module.
+We'll walk you through creating your first **command** module.
If you installed a new project via the CLI, your file should be here:
@@ -37,11 +36,10 @@ import { commandModule, CommandType } from "@sern/handler";
export default commandModule({
type: CommandType.Slash,
description: "A ping command",
- execute: async (ctx, sdt) => {
+ execute: async (ctx, sdt) => { // ctx is Context, sdt is SDT type
await ctx.reply("Pong 🏓");
},
});
-
```
@@ -75,7 +73,7 @@ So, lets say you want to make a command module that listens to **buttons**, or A
export default commandModule({
type: CommandType.Slash,
description: "A ping command",
- execute: async (ctx, sdt) => {
+ execute: async (ctx, sdt) => {
const editButton = new ButtonBuilder({
customId: "btn",
label: "Click me",
@@ -109,7 +107,7 @@ Components can carry metadata. This comes in handy when handling multiple compon
- ```diff title="src/commands/ping.js"
+ ```js title="src/commands/ping.js" {8}
import { CommandType, commandModule } from "@sern/handler";
import { ButtonStyle, ActionRowBuilder, ButtonBuilder } from 'discord.js';
export default commandModule({
@@ -117,8 +115,7 @@ Components can carry metadata. This comes in handy when handling multiple compon
description: "A ping command",
execute: async (ctx, sdt) => {
const editButton = new ButtonBuilder({
- - customId: "btn",
- + customId: "btn/1061421834341462036",
+ customId: "btn/1061421834341462036",
label: "Click me",
emoji: "🛠",
style: ButtonStyle.Primary,
@@ -148,3 +145,65 @@ Components can carry metadata. This comes in handy when handling multiple compon
:::tip
The first `/` is significant in custom ids. On the left of it is the custom id to be matched, and on the right is any user defined data.
:::
+
+## Event Modules
+We are now moving to event modules, which listens to the vast streams of data provided by
+- `node-cron`, `EventType.Cron`
+- `sern`, `EventType.Sern`
+- `discord.js`, `EventType.Discord`
+- `yourself`, `EventType.External`
+
+
+
+- src/events/
+ - **messageCreate.js** **(right here, probably)**
+- ...
+
+
+### Listening to Discord Events
+
+```js title="src/events/messageCreate.js"
+import { eventModule, EventType } from "@sern/handler";
+
+export default eventModule({
+ type: EventType.Discord,
+ execute: async (message) => {
+ console.log(`${message.user} said`, message.content)
+ },
+});
+```
+
+:::tip
+Typescript users can use `discordEvent`, a specialized eventModule with typings for discord.js events.
+:::
+
+
+### Scheduling with node-cron
+
+```js
+import { EventType, eventModule } from "@sern/handler";
+
+export default eventModule({
+ type: EventType.Cron,
+ pattern: "* * * * *", // Run this every minute
+ execute: (args) => {
+ console.log("cron cron")
+ }
+})
+```
+
+
+### Run once
+This works for ALL event modules.
+```js {6}
+import { EventType, eventModule } from "@sern/handler";
+
+export default eventModule({
+ type: EventType.Cron,
+ pattern: "* * * * *", // Run this every minute
+ once: true,
+ execute: (args) => {
+ console.log("cron cron")
+ }
+})
+```
diff --git a/src/content/docs/v4/reference/plugins.mdx b/src/content/docs/v4/reference/plugins.mdx
new file mode 100644
index 000000000..83809fa8b
--- /dev/null
+++ b/src/content/docs/v4/reference/plugins.mdx
@@ -0,0 +1,159 @@
+---
+title: Plugins
+description: Run code before execution
+sidebar:
+ order: 4
+---
+
+:::tip
+**TLDR:** Plugins are reusable pieces of code and are installable via `sern plugins`.
+They can modify the module's fields and also perform preconditions.
+Put them into the `plugins` field of a [module](../modules).
+:::
+
+## Installation
+
+Chances are, you just want your bot to work. Plugins can preprocess and create reusable conditions for modules.
+
+To install plugins, you can use the CLI:
+
+```sh
+sern plugins
+```
+:::tip
+Feel free to contribute to the [repository](https://github.com/sern-handler/awesome-plugins)!
+:::
+
+:::caution
+Some plugins only work with specific command types. Most, however, are targeted towards slash / both modules.
+:::
+
+import { Steps } from '@astrojs/starlight/components';
+
+
+1. Install your favorite(s) (or the ones that look the coolest). I installed the `ownerOnly` plugin.
+2. Thank the creator of the plugin. (mandatory)
+3. Add the plugin to your module in the `plugins` field.
+
+
+```ts title="src/commands/ping.ts" {6}
+import { commandModule, CommandType } from '@sern/handler'
+import { ownerOnly } from '../plugins'
+
+export default commandModule({
+ type: CommandType.Both,
+ plugins: [ownerOnly(['182326315813306368'])],
+ description: 'ping command',
+ execute: (ctx) => {
+ ctx.reply('hello, owner');
+ }
+})
+```
+┗|` O′|┛ perfect, your first plugin!
+
+## Creating Plugins
+
+Plugins are essentially functions that use the controller object to determine whether to continue or stop the execution of a command.
+
+## Init Plugins
+
+Init plugins modify how commands are loaded or do preprocessing.
+```ts title="src/plugins/updateDescription.js" {3} {9}
+import { CommandInitPlugin } from "@sern/handler";
+
+export const updateDescription = (description: string) => {
+ return CommandInitPlugin(({ updateModule, deps }) => {
+ if(description.length > 100) {
+ deps.logger?.info({ message: "Invalid description" })
+ console.error("Description is invalid")
+ return controller.stop("From updateDescription: description is invalid");
+ }
+ updateModule({ description });
+ return controller.next(); // continue to next plugin
+ });
+};
+```
+
+## Control Plugins
+
+
+
+
+```js
+import { CommandControlPlugin } from "@sern/handler";
+
+export const inGuild = (guildId: string) => {
+ return CommandInitPlugin(ctx, sdt) => {
+ if(ctx.guild.id !== guildId) {
+ return controller.stop();
+ }
+ return controller.next();
+ });
+};
+
+```
+
+1. An event is emitted by `discord.js`.
+2. This event is passed to all control plugins **in order!!**,
+3. If all are successful, the command is executed.
+
+
+:::note
+Calling `controller.stop()` notifies sern that this command should not be run, and command is ignored.
+:::
+
+:::tip
+Control Plugins are good for filtering, preconditions, parsing.
+:::
+
+
+### Controller Object
+
+The controller object is passed into every plugin. It has two methods: `next` and `stop`.
+
+Plugins use the controller to control the flow of the command. For example, if a plugin fails, it calls `controller.stop()` to prevent the command from executing.
+
+```ts
+// Reference object, import this from @sern/handler
+const controller = {
+ next: (val?: Record) => Ok(val),
+ stop: (val?: string) => Err(val),
+};
+```
+
+### Passing state with SDT
+> SDT = state, [dependencies](../dependencies), type (very creative)
+
+Controllers can pass data downstream. That is, plugins can recieve data from previous plugin calls.
+If all control plugins are successful, the final state is passed to the module `execute`.
+
+```js
+import { commandModule, CommandControlPlugin, CommandType } from '@sern/handler'
+
+const plugin = CommandControlPlugin((ctx, sdt) => {
+ return controller.next({ a: 'from plugin1' });
+});
+const plugin2 = CommandControlPlugin((ctx, sdt) => {
+ return controller.next({ b: ctx.user.id + "from plugin2" });
+})
+
+export default commandModule({
+ type: CommandType.Slash,
+ plugins: [plugin, plugin2],
+ execute: (ctx, sdt) => {
+ console.log(sdt.state) // { a: 'from plugin1', b: '182326315813306368 from plugin2' }
+ }
+})
+```
+
+:::tip
+State passing is a glorified asynchronous [reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce).
+:::
+
+#### Caveats
+Passing data with the same key will get overridden by the latest plugin.
+It is recommended to namespace data keys if you have multiple plugins, or you can ensure no keys are overridden by the
+plugin chain.
+```js
+return controller.next({ 'cheese-plugin/data' : "From cheese-plugin" })
+```
diff --git a/src/content/docs/v4/reference/presence.mdx b/src/content/docs/v4/reference/presence.mdx
new file mode 100644
index 000000000..0102f1601
--- /dev/null
+++ b/src/content/docs/v4/reference/presence.mdx
@@ -0,0 +1,93 @@
+---
+title: Presence
+description: Manage your bot's presence programatically
+sidebar:
+ order: 5
+---
+
+
+## Presence
+Your bot should have a personality. (or invite link)
+
+### Once
+
+Your presence will be set once, after discord.js `Client` is called.
+
+```js title="./src/presence.js"
+import { Presence } from '@sern/handler'
+import { ActivityType } from 'discord.js';
+
+const activity = { type: ActivityType.Listening, name: "what's bofa" };
+
+export default Presence.module({
+ execute: () => {
+ return Presence
+ .of({ activities: [activity], status: "idle" })
+ .once();
+ }
+ })
+```
+
+
+### Repeated
+
+Set your presence on intervals or events emitted.
+
+```js title="./src/presence.js"
+import { Presence } from '@sern/handler'
+import { ActivityType, ClientPresenceStatus } from 'discord.js';
+
+/**
+ * Sorry for using any[]
+ * @param array {any[]}
+ */
+function shuffleArray(array) {
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return [...array];
+}
+
+const statuses = [[ActivityType.Watching, "the sern community", "online"],
+ [ActivityType.Listening, "Evo", "dnd"],
+ [ActivityType.Playing, "with @sern/cli", "idle"],
+ [ActivityType.Watching, "sern bots", "dnd"],
+ [ActivityType.Watching, "github stars go brrr", "online"],
+ [ActivityType.Listening, "Spotify", "dnd"],
+ [ActivityType.Listening, "what's bofa", "idle"]];
+
+export default Presence.module({
+ execute: () => {
+ const [type, name, status] = statuses.at(0)!;
+ return Presence
+ //start your presence with this.
+ .of({ activities: [ { type, name } ], status })
+ .repeated(() => {
+ const [type, name, status] = [...shuffleArray(statuses)].shift()!;
+ return {
+ status,
+ activities: [{ type, name }]
+ };
+ }, 60_000); //repeat and setPresence with returned result every minute
+ }
+})
+```
+
+### Inject dependencies
+```js title="./src/presence.js" {7-8}
+import { Presence } from '@sern/handler'
+import { ActivityType } from 'discord.js';
+
+const activity = { type: ActivityType.Listening, name: "what's bofa" };
+export default Presence.module({
+ inject: ['@sern/logger'],
+ execute: (logger) => {
+ logger?.info({ message: "Presence changed" });
+ return Presence
+ .of({ activities: [activity], status: "idle" })
+ .once();
+ }
+ })
+```
+
diff --git a/src/content/docs/v4/reference/project-layout.mdx b/src/content/docs/v4/reference/project-layout.mdx
index 35e64d684..89623217e 100644
--- a/src/content/docs/v4/reference/project-layout.mdx
+++ b/src/content/docs/v4/reference/project-layout.mdx
@@ -2,7 +2,7 @@
title: Project Layout
description: The layout of a sern project
sidebar:
- order: 1
+ order: 2
---
Usually, a project should look like this: