Skip to content

Commit 9d1a133

Browse files
authored
Create discord bot (#1)
1 parent 1837767 commit 9d1a133

File tree

8 files changed

+1864
-0
lines changed

8 files changed

+1864
-0
lines changed

.gitignore

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
lerna-debug.log*
8+
.pnpm-debug.log*
9+
10+
# Diagnostic reports (https://nodejs.org/api/report.html)
11+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
*.lcov
25+
26+
# nyc test coverage
27+
.nyc_output
28+
29+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30+
.grunt
31+
32+
# Bower dependency directory (https://bower.io/)
33+
bower_components
34+
35+
# node-waf configuration
36+
.lock-wscript
37+
38+
# Compiled binary addons (https://nodejs.org/api/addons.html)
39+
build/Release
40+
41+
# Dependency directories
42+
node_modules/
43+
jspm_packages/
44+
45+
# Snowpack dependency directory (https://snowpack.dev/)
46+
web_modules/
47+
48+
# TypeScript cache
49+
*.tsbuildinfo
50+
51+
# Optional npm cache directory
52+
.npm
53+
54+
# Optional eslint cache
55+
.eslintcache
56+
57+
# Optional stylelint cache
58+
.stylelintcache
59+
60+
# Microbundle cache
61+
.rpt2_cache/
62+
.rts2_cache_cjs/
63+
.rts2_cache_es/
64+
.rts2_cache_umd/
65+
66+
# Optional REPL history
67+
.node_repl_history
68+
69+
# Output of 'npm pack'
70+
*.tgz
71+
72+
# Yarn Integrity file
73+
.yarn-integrity
74+
75+
# dotenv environment variable files
76+
.env
77+
.env.development.local
78+
.env.test.local
79+
.env.production.local
80+
.env.local
81+
82+
# parcel-bundler cache (https://parceljs.org/)
83+
.cache
84+
.parcel-cache
85+
86+
# Next.js build output
87+
.next
88+
out
89+
90+
# Nuxt.js build / generate output
91+
.nuxt
92+
dist
93+
94+
# Gatsby files
95+
.cache/
96+
# Comment in the public line in if your project uses Gatsby and not Next.js
97+
# https://nextjs.org/blog/next-9-1#public-directory-support
98+
# public
99+
100+
# vuepress build output
101+
.vuepress/dist
102+
103+
# vuepress v2.x temp and cache directory
104+
.temp
105+
.cache
106+
107+
# Docusaurus cache and generated files
108+
.docusaurus
109+
110+
# Serverless directories
111+
.serverless/
112+
113+
# FuseBox cache
114+
.fusebox/
115+
116+
# DynamoDB Local files
117+
.dynamodb/
118+
119+
# TernJS port file
120+
.tern-port
121+
122+
# Stores VSCode versions used for testing VSCode extensions
123+
.vscode-test
124+
125+
# yarn v2
126+
.yarn/cache
127+
.yarn/unplugged
128+
.yarn/build-state.yml
129+
.yarn/install-state.gz
130+
.pnp.*
131+
132+
#IntelliJ
133+
.idea

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Example to Send a Discord Message from a Render Webhook
2+
3+
This example sends a message to Discord when receiving a server failed webhook.
4+
5+
# Prerequisites
6+
If you haven't already, [sign up for a Render account](https://dashboard.render.com/register).
7+
Creating webhooks on Render requires a Professional plan or higher. You can [view and upgrade your plan](https://dashboard.render.com/billing/update-plan) in the Render Dashboard.
8+
9+
## Deploy to Render
10+
11+
1. Use the button below to deploy to Render </br>
12+
<a href="https://render.com/deploy?repo=https://github.com/render-examples/webhook-discord-bot/tree/main"><img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render"></a>
13+
14+
2. Follow [instructions](https://render.com/docs/webhooks) to create a webhook with the URL from your service and `/webhook` path
15+
3. Follow [instructions](https://render.com/docs/api#1-create-an-api-key) to create a Render API Key
16+
4. Follow [instructions](https://discord.com/developers/docs/quick-start/getting-started#step-1-creating-an-app) to create a Discord App and copy the token
17+
5. Navigate to the installation settings for your app and
18+
- add `bot` scope
19+
- add `SendMessages` and `ViewChannels` permissions
20+
6. Set the following env vars
21+
- `RENDER_WEBHOOK_SECRET` environment variable to the secret from the webhook created in step 2
22+
- `RENDER_API_KEY` to the key created in step 3
23+
- `DISCORD_TOKEN` to the token created in step 4
24+
- `DISCORD_CHANNEL_ID` to the channel id you want messages sent to
25+
26+
## Developing
27+
28+
Once you've created a project and installed dependencies with `pnpm install`, start a development server:
29+
30+
```bash
31+
pnpm run dev
32+
```
33+
34+
## Building
35+
36+
```bash
37+
pnpm run build
38+
```

app.ts

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import express, {NextFunction, Request, Response} from "express";
2+
import {Webhook, WebhookUnbrandedRequiredHeaders, WebhookVerificationError} from "standardwebhooks"
3+
import {RenderEvent, RenderService, WebhookPayload} from "./render";
4+
import {
5+
ActionRowBuilder,
6+
ButtonBuilder,
7+
ButtonStyle,
8+
Client,
9+
EmbedBuilder,
10+
Events,
11+
GatewayIntentBits,
12+
MessageActionRowComponentBuilder
13+
} from "discord.js";
14+
15+
const app = express();
16+
const port = process.env.PORT || 3001;
17+
const renderWebhookSecret = process.env.RENDER_WEBHOOK_SECRET || '';
18+
19+
const renderAPIURL = process.env.RENDER_API_URL || "https://api.render.com/v1"
20+
21+
// To create a Render API token, follow instructions here: https://render.com/docs/api#1-create-an-api-key
22+
const renderAPIToken = process.env.RENDER_API_TOKEN || '';
23+
24+
const discordToken = process.env.DISCORD_TOKEN || '';
25+
const discordChannelID = process.env.DISCORD_CHANNEL_ID || '';
26+
27+
// Create a new client instance
28+
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
29+
30+
// When the client is ready, run this code (only once).
31+
// The distinction between `client: Client<boolean>` and `readyClient: Client<true>` is important for TypeScript developers.
32+
// It makes some properties non-nullable.
33+
client.once(Events.ClientReady, readyClient => {
34+
console.log(`Discord client setup! Logged in as ${readyClient.user.tag}`);
35+
});
36+
37+
// Log in to Discord with your client's token
38+
client.login(discordToken).catch(err => {
39+
console.error(`unable to connect to Discord: ${err}`);
40+
});
41+
42+
app.post("/webhook", express.raw({type: 'application/json'}), (req: Request, res: Response, next: NextFunction) => {
43+
try {
44+
validateWebhook(req);
45+
} catch (error) {
46+
return next(error)
47+
}
48+
49+
const payload: WebhookPayload = JSON.parse(req.body)
50+
51+
res.status(200).send({}).end()
52+
53+
// handle the webhook async so we don't timeout the request
54+
handleWebhook(payload)
55+
});
56+
57+
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
58+
console.error(err);
59+
if (err instanceof WebhookVerificationError) {
60+
res.status(400).send({}).end()
61+
} else {
62+
res.status(500).send({}).end()
63+
}
64+
});
65+
66+
const server = app.listen(port, () => console.log(`Example app listening on port ${port}!`));
67+
68+
function validateWebhook(req: Request) {
69+
const headers: WebhookUnbrandedRequiredHeaders = {
70+
"webhook-id": req.header("webhook-id") || "",
71+
"webhook-timestamp": req.header("webhook-timestamp") || "",
72+
"webhook-signature": req.header("webhook-signature") || ""
73+
}
74+
75+
const wh = new Webhook(renderWebhookSecret);
76+
wh.verify(req.body, headers);
77+
}
78+
79+
async function handleWebhook(payload: WebhookPayload) {
80+
try {
81+
switch (payload.type) {
82+
case "server_failed":
83+
const service = await fetchServiceInfo(payload)
84+
const event = await fetchEventInfo(payload)
85+
86+
console.log(`sending discord message for ${service.name}`)
87+
await sendServerFailedMessage(service, event.details.reason)
88+
return
89+
default:
90+
console.log(`unhandled webhook type ${payload.type} for service ${payload.data.serviceId}`)
91+
}
92+
} catch (error) {
93+
console.error(error)
94+
}
95+
}
96+
97+
async function sendServerFailedMessage(service: RenderService, failureReason: any) {
98+
const channel = await client.channels.fetch(discordChannelID);
99+
if (!channel ){
100+
throw new Error(`unable to find specified Discord channel ${discordChannelID}`);
101+
}
102+
103+
const isSendable = channel.isSendable()
104+
if (!isSendable) {
105+
throw new Error(`specified Discord channel ${discordChannelID} is not sendable`);
106+
}
107+
108+
let description = "Failed for unknown reason"
109+
if (failureReason.nonZeroExit) {
110+
description = `Exited with status ${failureReason.nonZeroExit}`
111+
} else if (failureReason.oomKilled) {
112+
description = `Out of Memory`
113+
} else if (failureReason.timedOutSeconds) {
114+
description = `Timed out ` + failureReason.timedOutReason
115+
} else if (failureReason.unhealthy) {
116+
description = failureReason.unhealthy
117+
}
118+
119+
const embed = new EmbedBuilder()
120+
.setColor(`#FF5C88`)
121+
.setTitle(`${service.name} Failed`)
122+
.setDescription(description)
123+
.setURL(service.dashboardUrl)
124+
125+
const logs = new ButtonBuilder()
126+
.setLabel("View Logs")
127+
.setURL(`${service.dashboardUrl}/logs`)
128+
.setStyle(ButtonStyle.Link);
129+
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>()
130+
.addComponents(logs);
131+
132+
channel.send({embeds: [embed], components: [row]})
133+
}
134+
135+
// fetchEventInfo fetches the event that triggered the webhook
136+
// some events have additional information that isn't in the webhook payload
137+
// for example, deploy events have the deploy id
138+
async function fetchEventInfo(payload: WebhookPayload): Promise<RenderEvent> {
139+
const res = await fetch(
140+
`${renderAPIURL}/events/${payload.data.id}`,
141+
{
142+
method: "get",
143+
headers: {
144+
"Content-Type": "application/json",
145+
Accept: "application/json",
146+
Authorization: `Bearer ${renderAPIToken}`,
147+
},
148+
},
149+
)
150+
if (res.ok) {
151+
return res.json()
152+
} else {
153+
throw new Error(`unable to fetch event info; received code :${res.status.toString()}`)
154+
}
155+
}
156+
157+
async function fetchServiceInfo(payload: WebhookPayload): Promise<RenderService> {
158+
const res = await fetch(
159+
`${renderAPIURL}/services/${payload.data.serviceId}`,
160+
{
161+
method: "get",
162+
headers: {
163+
"Content-Type": "application/json",
164+
Accept: "application/json",
165+
Authorization: `Bearer ${renderAPIToken}`,
166+
},
167+
},
168+
)
169+
if (res.ok) {
170+
return res.json()
171+
} else {
172+
throw new Error(`unable to fetch service info; received code :${res.status.toString()}`)
173+
}
174+
}
175+
176+
process.on('SIGTERM', () => {
177+
console.debug('SIGTERM signal received: closing HTTP server')
178+
server.close(() => {
179+
console.debug('HTTP server closed')
180+
})
181+
})

package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "webhook-discord-bot",
3+
"version": "1.0.0",
4+
"description": "Render Webhook Receiver Example",
5+
"main": "app.ts",
6+
"repository": "https://github.com/render-examples/webhook-discord-bot",
7+
"author": "Render Developers",
8+
"license": "MIT",
9+
"private": false,
10+
"scripts": {
11+
"build": "pnpm exec tsc",
12+
"start": "node dist/app.js",
13+
"dev": "tsnd --respawn app.ts",
14+
"typecheck": "tsc --noEmit --pretty"
15+
},
16+
"keywords": [],
17+
"dependencies": {
18+
"discord.js": "^14.18.0",
19+
"express": "^5.0.1",
20+
"standardwebhooks": "^1.0.0"
21+
},
22+
"devDependencies": {
23+
"@types/express": "^5.0.0",
24+
"@types/node": "^22.13.2",
25+
"ts-node-dev": "^2.0.0",
26+
"typescript": "^5.7.3"
27+
}
28+
}

0 commit comments

Comments
 (0)