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 + +![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(); + }); +}; + +``` + +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: