Back to APIs list

Discord API examples & templates

Use these vals as a playground to view and fork Discord API examples and templates on Val Town. Run any example below or find templates that can be used as a pre-built solution.
1
2
// set at Tue Feb 27 2024 23:09:43 GMT+0000 (Coordinated Universal Time)
export let discordWelcomedMembers = ["810866199536861224","399192125000122410","208758630642614272","168114361586548739","693604532877525033","1037134219085885480","836849743677751306","928433034716385301","865264239413690378","530789581390479390","2649095
1
2
// set at Tue Feb 27 2024 23:09:43 GMT+0000 (Coordinated Universal Time)
export let discordWelcomeBotStartedAt = 1687289563521;

Discord Notification for new Stripe Subscription

This endpoint gets webhooks from Stripe when we get a new subscriber to Val Town pro. It get's the customer's email address and let's the team know.

TODO

  • Send the user a thank you email from me
  • Get the user's name and val town username
  • Get a list of the user's vals
Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/** @jsxImportSource https://esm.sh/react */
import { email } from "https://esm.town/v/std/email?v=11";
import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
import { html } from "https://esm.town/v/stevekrouse/html";
import { thisValURL } from "https://esm.town/v/stevekrouse/thisValURL";
import { renderToString } from "npm:react-dom/server";
import Stripe from "npm:stripe";
const stripe = new Stripe(Deno.env.get("stripe_sk_customer_readonly") as string, {
apiVersion: "2020-08-27",
});
function getStripeCustomer(customerId) {
return stripe.customers.retrieve(customerId);
}
let welcomeEmail = renderToString(
<div style={{ maxWidth: "500px", fontFamily: "sans-serif" }}>
<p>
I just want to thank you for becoming a paying customer of Val Town. It's your support like yours that enables us to build the future of coding.
</p>
<p>If I can ever do anything for you, let me know! And if you have any feedback please send it my way.</p>
<p>Best,</p>
<p>Steve</p>
<p>Val Town | CEO</p>
<p>
ps - this email is <a href={thisValURL()}>a val</a>
</p>
</div>,
);
export let newStripeEvent = async (req: Request) => {
if (req.method === "GET") return html(welcomeEmail);
const signature = req.headers.get("Stripe-Signature");
const body = await req.text();
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
let event;
try {
event = await stripe.webhooks.constructEventAsync(
body,
signature,
webhookSecret,
);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
const customer = await getStripeCustomer(event.data.object.customer);
const customerEmail = customer.email;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// https://api.val.town/v1/express/liamdanielduffy.reactTodoListWebsite
export const REACT_TODO_LIST_CONTENTS = {
body: `<div id="root"></div>
<script>
const e = React.createElement;
class TodoList extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], text: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
render() {
return e('div', {
id: 'todoContainer'
}, [
e('h3', {}, 'TODO'),
e('ul', {}, this.state.items.map((item, i) =>
e('li', {key: i}, [
e('input', { type: 'checkbox', id: 'item' + i, name: 'item' + i }),
e('label', { htmlFor: 'item' + i }, item)
])
)),
e('form', {onSubmit: this.handleSubmit}, [
e('input', {
id: 'new-todo',
onChange: this.handleChange,
value: this.state.text,
}),
e('button', {}, 'Add Todo #' + (this.state.items.length + 1)),
]),
]);
}
handleChange(e) {
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
if (this.state.text.length === 0) {
return;
}
const newItem = this.state.text;
this.setState(state => ({
items: state.items.concat(newItem),
text: ''
}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { slackPost } from "https://esm.town/v/glommer/sendMsgToSlack";
import { sqlite } from "https://esm.town/v/std/sqlite";
import { sql } from "npm:drizzle-orm";
import { drizzle } from "npm:drizzle-orm/libsql";
import { integer, sqliteTable, text } from "npm:drizzle-orm/sqlite-core";
const db = drizzle(sqlite as any);
const threadsTbl = sqliteTable("threads", {
id: text("id").primaryKey(),
last_message_id: text("last_message_id"),
});
async function getKnownThreads() {
const result = await db.select().from(threadsTbl);
return result.reduce((r, { id, last_message_id }) => {
r[id] = parseInt(last_message_id ?? "0");
return r;
}, {});
}
async function getNewThreads() {
const resp = await fetch(`https://discord.com/api/guilds/${guild}/threads/active`, {
headers: {
"Authorization": `Bot ${token}`,
},
});
if (!resp.ok) {
const err = await resp.json();
console.log(`response failed: ${err}`);
return;
}
const j = await resp.json();
return j.threads;
}
export async function getThreadsActivity(
token: string,
guild: string,
slackToken: string,
slackChannel: string,
) {
const knownThreads = await getKnownThreads();
const threads = await getNewThreads();
const ops = [db.delete(threadsTbl)];
for (let i = 0; i < threads.length; i++) {
const t = threads[i];
const id = t.id;
const name = t.name;
// don't do parseInt here, need to be text, so we can save text back to sqlite
const last_message_id = t.last_message_id;
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { verify_discord_signature } from "https://esm.town/v/mattx/verify_discord_signature";
import { formatInput } from "https://esm.town/v/rayman/formatInput";
import { fetch } from "https://esm.town/v/std/fetch";
import { example1 } from "https://esm.town/v/stevekrouse/example1?v=3";
import process from "node:process";
export let mgsrBotEndpoint = async (
req: express.Request,
res: express.Response,
) => {
console.log(req.get("X-Signature-Timestamp"));
console.log(req.get("X-Signature-Ed25519"));
if (!req.get("X-Signature-Timestamp") || !req.get("X-Signature-Ed25519")) {
res.status(400);
res.end("Signature headers missing!");
}
const verified = await verify_discord_signature(
process.env.discord_pubkey,
JSON.stringify(req.body),
req.get("X-Signature-Ed25519"),
req.get("X-Signature-Timestamp"),
);
if (!verified) {
res.status(401);
res.end("signature invalid");
return;
}
res.set("Content-type", "application/json");
switch (req.body.type) {
case 1: // PING interaction
res.json({ type: 1 }); // PONG
break;
case 2: // APPLICATION_COMMAND interactions
// TODO: Probably don't use a nested switch here
switch (req.body.data.name) {
case "ping":
res.json({
type: 4,
data: {
content: `Hello World! example1 is: ${example1}`,
},
});
break;
case "eval":
res.end(JSON.stringify({
type: 4,
data: {
content: `${
JSON.stringify(
await (await fetch(

Forward Render Error Emails to Val Town's Engineering Discord Channel

shapes at 24-02-08 12.23.08.png

Render sends emails when deploys fail but I want those notifications to come in our team Discord channel, and tag our team. Render doesn't have webhooks for this, so I set up a Gmail filter and forward to this email handler val, which in turn forwards the content to our engineering Discord channel.

Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
export default async function(email: Email) {
console.log(JSON.stringify(email));
if (email.from !== "Render <no-reply@render.com>") return "Unauthorized";
discordWebhook({
url: Deno.env.get("engDiscord"),
content: `<@&1081224342110736394> Render Email: ${email.subject}`,
});
}
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { verify_discord_signature } from "https://esm.town/v/mattx/verify_discord_signature";
import { formatInput } from "https://esm.town/v/rayman/formatInput";
import { fetch } from "https://esm.town/v/std/fetch";
import { example1 } from "https://esm.town/v/stevekrouse/example1?v=3";
import process from "node:process";
export async function mgsrBotEndpoint(request) {
const signatureTimestamp = request.headers.get("X-Signature-Timestamp");
const signatureEd25519 = request.headers.get("X-Signature-Ed25519");
console.log(signatureTimestamp);
console.log(signatureEd25519);
if (!signatureTimestamp || !signatureEd25519) {
return new Response("Signature headers missing!", { status: 400 });
}
const body = await request.json();
const verified = await verify_discord_signature(
process.env.discord_pubkey,
JSON.stringify(body),
signatureEd25519,
signatureTimestamp,
);
if (!verified) {
return new Response("Signature invalid", { status: 401 });
}
let responseBody;
let status = 200;
switch (body.type) {
case 1: // PING interaction
responseBody = JSON.stringify({ type: 1 }); // PONG
break;
case 2: // APPLICATION_COMMAND interactions
// Handling of different application commands
responseBody = await handleApplicationCommands(body);
if (!responseBody) {
status = 400;
responseBody = "bad request";
}
break;
default:
status = 400;
responseBody = "bad request";
break;
}
return new Response(responseBody, {
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { email } from "https://esm.town/v/std/email";
import { noteToSelf } from "https://esm.town/v/wittjosiah/discord_note_to_self";
import { parse } from "https://esm.town/v/wittjosiah/parse_github_email";
export default async function(message: Email) {
console.log(JSON.stringify(message, null, 2));
if (!message.from.endsWith("<notifications@github.com>")) {
return;
}
try {
const { title, description, url, name, bot, reason } = parse(message);
if (bot) {
return;
}
const body = {
embeds: [{
title,
description,
url,
author: {
name,
},
footer: {
text: reason,
},
}],
};
const { status, dmId } = await noteToSelf({
body,
dmId: Deno.env.get("DISCORD_DM_ID"),
userId: Deno.env.get("DISCORD_USER_ID"),
token: Deno.env.get("DISCORD_GH_BOT_TOKEN"),
});
if (dmId) {
Deno.env.set("DISCORD_DM_ID", dmId);
}
} catch (err) {
console.error(err);
email({ text: message.text });
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export async function noteToSelf(
{ body, dmId, userId, token }: { body: object; dmId: string; userId: string; token: string },
) {
if (!dmId) {
console.log("Fetching dmId...", { userId });
const channelResponse = await fetch("https://discord.com/api/users/@me/channels", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${token}`,
},
body: JSON.stringify({ recipient_id: userId }),
});
if (channelResponse.status !== 200) {
return { status: channelResponse.status, dmId: undefined };
}
const { id: channelId } = await channelResponse.json();
dmId = channelId;
}
const dmResponse = await fetch(`https://discord.com/api/channels/${dmId}/messages`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${token}`,
},
body: JSON.stringify(body),
});
return { status: dmResponse.status, dmId };
}

inTheBackground

With the addition of the "early return" feature of web handlers, you can now process short background tasks in vals. This can be really useful for occasions where an immediate response is required, with a subsequent update a few seconds later

e.g. a Discord bot that calls ChatGPT needs to respond within a few seconds, which can be too fast for the AI to generate a response. We can instead reply immediately and then update that message later, inTheBackground

Simply wrap something in inTheBackground and it will do just that! In this example, we log something a few seconds later than the web response is sent back.

Readme
Fork
1
2
3
4
5
6
7
8
9
10
import { inTheBackground } from "https://esm.town/v/neverstew/inTheBackground";
import { sleep } from "https://esm.town/v/neverstew/sleep";
export default async function(req: Request): Promise<Response> {
inTheBackground(async () => {
await sleep(3000);
console.info(new Date());
});
return Response.json({ time: new Date() });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { slackPost } from "https://esm.town/v/glommer/sendMsgToSlack";
import { sqlite } from "https://esm.town/v/std/sqlite";
import { sql } from "npm:drizzle-orm";
import { drizzle } from "npm:drizzle-orm/libsql";
import { integer, sqliteTable, text } from "npm:drizzle-orm/sqlite-core";
const db = drizzle(sqlite as any);
const threadsTbl = sqliteTable("threads", {
id: text("id").primaryKey(),
last_message_id: text("last_message_id"),
});
async function getKnownThreads() {
const result = await db.select().from(threadsTbl);
return result.reduce((r, { id, last_message_id }) => {
r[id] = parseInt(last_message_id ?? "0");
return r;
}, {});
}
async function getNewThreads() {
const resp = await fetch(`https://discord.com/api/guilds/${guild}/threads/active`, {
headers: {
"Authorization": `Bot ${token}`,
},
});
if (!resp.ok) {
const err = await resp.json();
console.log(`response failed: ${err}`);
return;
}
const j = await resp.json();
return j.threads;
}
export async function getThreadsActivity(
token: string,
guild: string,
slackToken: string,
slackChannel: string,
) {
const knownThreads = await getKnownThreads();
const threads = await getNewThreads();
const ops = [db.delete(threadsTbl)];
for (let i = 0; i < threads.length; i++) {
const t = threads[i];
const id = t.id;
const name = t.name;
// don't do parseInt here, need to be text, so we can save text back to sqlite
const last_message_id = t.last_message_id;

Send a Discord message

Send a message to a Discord channel from Val Town. It's useful for notifying your team or community when someone interesting happens, like a user signup, Stripe payment, mention on social media, etc.

@stevekrouse.discordWebhook({
  url: @me.secrets.discordWebhook,
  content: "Hi from val town!",
});

Example val: https://www.val.town/v/stevekrouse.discordWebhookEx

Setup

1. Create a Discord Webhook

Follow the instructions here: https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks

It really only takes 2 minutes.

2. Copy webhook URL

Paste it into your secrets as discordWebhook.

3. Send a message!

export const discordWebhookEx = @stevekrouse.discordWebhook({
  url: @me.secrets.discordWebhook,
  content: "Hi from val town!",
});

Example val: https://www.val.town/v/stevekrouse.discordWebhookEx

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { fetchText } from "https://esm.town/v/stevekrouse/fetchText?v=5"; // pin to proxied fetch
export const discordWebhook = async ({
url,
content,
}: {
url: string;
content: string;
}) => {
const text = await fetchText(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: content.slice(0, 2000) }),
});
if (text.length) throw Error("Discord Webhook error: " + text);
};
Runs every 15 min
Fork
1
2
3
4
5
6
7
8
9
10
import { getThreadsActivity } from "https://esm.town/v/glommer/getThreadsActivity";
export default async function(interval: Interval) {
const guild = Deno.env.get("DISCORD_GUILD_ID");
const token = Deno.env.get("DISCORD_IKU_BOT_TOKEN");
const slackToken = Deno.env.get("SLACK_TOKEN");
const channel = Deno.env.get("SLACK_NOTIFY_CHANNEL_ID");
await getThreadsActivity(token, guild, slackToken, channel);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export let date_me_docs_cache = [{
"Name": "Kamil",
"Profile": "https://docs.google.com/document/d/1kQgNZ7ikgKAxyxbsLSC2bBhXQgxv6Em340-ht5R5sjo/edit?usp=sharing",
"Gender": ["M"],
"Age": "24",
"Contact": "postreduxx gmail",
"LastUpdated": "2023-10-19T12:50:00.000Z",
"InterestedIn": ["M"],
"Location": ["Central Europe"],
"Style": ["mono"],
"LocationFlexibility": "Some",
"Community": ["Rationalism"],
}, {
"Name": "Zak Kallenborn",
"Profile": "zkallenborn.com/dateme",
"Gender": ["M"],
"Age": "33",
"Contact": "zkallenborn@gmail.com",
"LastUpdated": "2023-10-18T04:22:00.000Z",
"InterestedIn": ["F"],
"Location": ["DC"],
"Style": ["mono"],
"LocationFlexibility": "Some",
"Community": ["Tech", "EA"],
}, {
"Name": "Frazer Kirkman",
"Profile": "https://www.okcupid.com/profile/frazerkirkman",
"Gender": ["M"],
"Age": "44",
"Contact": "Frazerkirkman@gmail.com",
"LastUpdated": "2023-10-16T20:02:00.000Z",
"InterestedIn": ["F"],
"Location": ["San Francisco Bay Area"],
"Style": ["poly"],
"LocationFlexibility": "Some",
"Community": ["Tech", "Rationalism", "EA"],
}, {
"Name": "Mitchell Reynolds",
"Profile": "https://tundra-shell-d74.notion.site/Mitchell-s-Date-Me-Doc-9325999704f347a4b2515df774b3c7ff",
"Gender": ["M"],
"Age": "32",
"Contact": "https://www.facebook.com/mitchellsreynolds/",
"LastUpdated": "2023-10-14T06:43:00.000Z",
"InterestedIn": ["F"],
"Location": ["San Francisco Bay Area", "Berkeley"],
"Style": ["mono"],
"LocationFlexibility": "Flexible",
"Community": ["EA"],
}, {
"Name": "Sarah Hirner",