This commit is contained in:
Jacob Nguyen
2024-05-27 15:20:07 -05:00
parent ee8d2b3960
commit 5abf123935
8 changed files with 469 additions and 23 deletions

View File

@@ -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. 🎉
</Steps>

View File

@@ -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!

View File

@@ -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';
<FileTree>
- src/
- index.js **(your main file and client)**
- **dependencies.d.ts** **(for intellisense)**
</FileTree>
```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';
<Steps>
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. 🎉
</Steps>
### Dispose
> Your object needs to destroy things before shutdown, if a crash occurs
<Steps>
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. 🎉
</Steps>
:::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/)

View File

@@ -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';

View File

@@ -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
<Tabs syncKey="language-preference">
<TabItem value="js" label="Send Component with Custom Id">
```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`
<FileTree>
- src/events/
- **messageCreate.js** **(right here, probably)**
- ...
</FileTree>
### 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")
}
})
```

View File

@@ -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';
<Steps>
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.
</Steps>
```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
![control plugins](~/assets/docs/event-plugins.png)
```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();
});
};
```
<Steps>
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.
</Steps>
:::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<string,unknown>) => 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" })
```

View File

@@ -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();
}
})
```

View File

@@ -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: <br />