<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value=".actorcore
node_modules
">
<input type="hidden" name="project[files][README.md]" value="# Discord Bot Gateway for RivetKit

Example project demonstrating Discord Gateway lifecycle management with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 22+
- Discord application with a bot token and the **Message Content Intent** enabled

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/bots
npm install
```

### Development

```sh
DISCORD_BOT_TOKEN=your-token npm run dev
```

Invite the bot to a server, then send messages such as &quot;hello&quot; or &quot;ping&quot; in a text channel to see automatic replies and state updates.

## License

Apache 2.0
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-bots&quot;,&quot;version&quot;:&quot;2.0.20&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;tsx src/server.ts&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;},&quot;devDependencies&quot;:{&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivetkit/rivetkit@54999214372f9ecb3f4251a3be45c7a0eb8dfacf&quot;,&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.7.3&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;},&quot;dependencies&quot;:{&quot;@hono/node-server&quot;:&quot;^1.14.3&quot;,&quot;hono&quot;:&quot;^4.7.0&quot;},&quot;stableVersion&quot;:&quot;0.8.0&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    &quot;target&quot;: &quot;esnext&quot;,
    /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    &quot;lib&quot;: [&quot;esnext&quot;],
    /* Specify what JSX code is generated. */
    &quot;jsx&quot;: &quot;react-jsx&quot;,

    /* Specify what module code is generated. */
    &quot;module&quot;: &quot;esnext&quot;,
    /* Specify how TypeScript looks up a file from a given module specifier. */
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    /* Specify type package names to be included without being referenced in a source file. */
    &quot;types&quot;: [&quot;node&quot;],
    /* Enable importing .json files */
    &quot;resolveJsonModule&quot;: true,

    /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    &quot;allowJs&quot;: true,
    /* Enable error reporting in type-checked JavaScript files. */
    &quot;checkJs&quot;: false,

    /* Disable emitting files from a compilation. */
    &quot;noEmit&quot;: true,

    /* Ensure that each file can be safely transpiled without relying on other imports. */
    &quot;isolatedModules&quot;: true,
    /* Allow &#39;import x from y&#39; when a module doesn&#39;t have a default export. */
    &quot;allowSyntheticDefaultImports&quot;: true,
    /* Ensure that casing is correct in imports. */
    &quot;forceConsistentCasingInFileNames&quot;: true,

    /* Enable all strict type-checking options. */
    &quot;strict&quot;: true,

    /* Skip type checking all .d.ts files. */
    &quot;skipLibCheck&quot;: true
  },
  &quot;include&quot;: [&quot;src/**/*.ts&quot;, &quot;scripts/**/*.ts&quot;, &quot;tests/**/*.ts&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][src/registry.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;
import type { WorkspaceInput, WorkspaceState } from &quot;./types&quot;;
import { sendSlackMessage } from &quot;./utils&quot;;

const DAY_IN_MS = 86_400_000;

export const slackWorkspaceBot = actor({
	createState: async (c, input): Promise&lt;WorkspaceState&gt; =&gt; {
		// Schedule first daily report
		const nextReportAt = Date.now() + DAY_IN_MS;
		await c.schedule.at(nextReportAt, &quot;sendDailyReport&quot;);

		return {
			workspaceId: (input as WorkspaceInput).workspaceId,
			channelId: (input as WorkspaceInput).channelId,
			messageCount: 0,
			nextReportAt,
		};
	},

	actions: {
		// Called by the Slack webhook in the Hono server
		handleMessage: async (c, text: string) =&gt; {
			c.state.messageCount++;

			const msg = text.toLowerCase().trim();
			let response: string | undefined;

			if (msg === &quot;ping&quot;) {
				response = &quot;pong&quot;;
			} else if (msg === &quot;count&quot;) {
				response = `I&#39;ve received ${c.state.messageCount} messages in this workspace`;
			} else if (msg === &quot;help&quot;) {
				response =
					&quot;Available commands:\n• ping - responds with pong\n• count - shows message count\n• help - shows this message&quot;;
			}

			if (response) {
				await sendSlackMessage(c.state.channelId, response);
			}
		},

		sendDailyReport: async (c) =&gt; {
			// Schedule next report
			const nextReportAt = Date.now() + DAY_IN_MS;
			c.state.nextReportAt = nextReportAt;
			await c.schedule.at(nextReportAt, &quot;sendDailyReport&quot;);

			// Send report to Slack if we have a channel
			if (c.state.channelId) {
				const report = `Daily report: ${c.state.messageCount} messages received so far`;
				await sendSlackMessage(c.state.channelId, report);
			}
		},
	},
});

export const registry = setup({
	use: { slackWorkspaceBot },
});

export type Registry = typeof registry;
">
<input type="hidden" name="project[files][src/server.ts]" value="import { serve } from &quot;@hono/node-server&quot;;
import { Hono } from &quot;hono&quot;;
import { registry } from &quot;./registry&quot;;

const { client } = registry.start();

const app = new Hono();

app.post(&quot;/slack/events&quot;, async (c) =&gt; {
	const body = await c.req.json();

	if (body.type === &quot;url_verification&quot;) {
		return c.json({ challenge: body.challenge });
	}

	if (body.type === &quot;event_callback&quot; &amp;&amp; body.event.type === &quot;message&quot;) {
		const workspaceId = body.team_id;
		const channelId = body.event.channel;

		const bot = client.slackWorkspaceBot.getOrCreate(workspaceId, {
			createWithInput: {
				workspaceId,
				channelId,
			},
		});

		await bot.handleMessage(body.event.text);

		return c.json({ ok: true });
	}

	return c.json({ ok: false }, 400);
});

serve({ fetch: app.fetch, port: 8080 });
console.log(&quot;Slack bot listening on port 8080&quot;);
">
<input type="hidden" name="project[files][src/types.ts]" value="export type WorkspaceInput = {
	workspaceId: string;
	channelId: string;
};

export type WorkspaceState = WorkspaceInput &amp; {
	messageCount: number;
	nextReportAt?: number;
};
">
<input type="hidden" name="project[files][src/utils.ts]" value="export async function sendSlackMessage(
	channelId: string,
	text: string,
): Promise&lt;void&gt; {
	const token = process.env.SLACK_BOT_TOKEN;
	if (!token) {
		return;
	}

	await fetch(&quot;https://slack.com/api/chat.postMessage&quot;, {
		method: &quot;POST&quot;,
		headers: {
			Authorization: `Bearer ${token}`,
			&quot;Content-Type&quot;: &quot;application/json&quot;,
		},
		body: JSON.stringify({
			channel: channelId,
			text,
		}),
	});
}
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="example-bots">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>