Back to APIs list

Slack API examples & templates

Use these vals as a playground to view and fork Slack 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
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
export const queryParams = (req: Request) => {
const searchParams = new URL(req.url).searchParams;
return Response.json({
"headers": Object.fromEntries(req.headers.entries()),
"url": req.url,
"method": req.method,
"mode": req.mode,
"cache": req.cache,
"credentials": req.credentials,
"body": req.body,
"destination": req.destination,
"integrity": req.integrity,
"referrer": req.referrer,
"redirect": req.redirect,
"searchParams": Object.fromEntries(searchParams.entries()),
"docs": "https://developer.mozilla.org/en-US/docs/Web/API/Request",
});
};
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
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
export const slackReplyToMessage = async (req: Request) => {
const body = await req.json();
// Verify the request is genuine
if (body.token !== Deno.env.get("slackVerificationToken")) {
return new Response(undefined, { status: 401 });
}
// Respond to the initial challenge (when events are enabled)
if (body.challenge) {
return Response.json({ challenge: body.challenge });
}
// Reply to app_mention events
if (body.event.type === "app_mention") {
// Note: `body.event` has information about the event
// like the sender and the message text
const result = await fetchJSON(
"https://slack.com/api/chat.postMessage",
{
headers: {
"Authorization": `Bearer ${Deno.env.get("CVEslackToken")}`,
},
method: "POST",
body: JSON.stringify({
channel: body.event.channel,
thread_ts: body.event.ts,
text: "Hello, ~World~ from Val Town!",
}),
},
);
// Slack replies with information about the created message
console.log(result);
}
};

Sends a daily message to the team Slack channel. Similar to 37signals' What did you work on today?

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { fetch } from "https://esm.town/v/std/fetch";
import { getDayName } from "https://esm.town/v/stevekrouse/getDayName?v=2";
import process from "node:process";
export const dailySlackRoundup = (async () => {
const res = await fetch(process.env.BRAINBOT_WEBHOOK_URL, {
method: "POST",
body: JSON.stringify({
"text":
`✌️ *Time for the ${getDayName()} round-up!* If you've worked today, what did you work on? Anything in your way?`,
}),
});
return res.statusText;
});
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;
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
export async function slackPost(token, channel, text) {
const resp = await fetch(
`https://slack.com/api/chat.postMessage`,
{
method: "POST",
headers: {
'Authorization': `Bearer ${token}`,
'Content-type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
channel,
text
})
})
if (!resp.ok) {
let { message } = await resp.json();
throw new Error("Error: " + message);
}
return await resp.json();
}

Val Town inspiration & use cases list

This list is fairly out of date. I'd love to accept pull requests with new or better vals!

Readme
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 valTownInspoList = [{
"title": "What can I make in Val Town?",
"description": "The data for this page was created by using Val Town as a CMS",
"val": "@rodrigotello.valTownInspoList",
image:
"https://air-prod.imgix.net/abff23bf-fc18-485f-a6b5-4a1c8b7f91ec.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "NASA photo of the day.",
"description": "Returns NASA's Astronomy Picture of the Day (APOD)",
"val": "@rodrigotello.nasaAPOD",
"image":
"https://air-prod.imgix.net/11ab8b2e-c052-4574-8341-96d5d74040cc.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "hnFollow",
"description": "Get email notifications any time an author you follow posts in Hacker News.",
"val": "@rodrigotello.hnFollow",
"image":
"https://air-prod.imgix.net/61e08fed-c77e-4a7b-9ae5-1efabb349574.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "GitHub events",
"description": "Get a GithHub users' public events",
"val": "@stevekrouse.githubEvents",
image:
"https://air-prod.imgix.net/3545d2cb-7cae-49d9-98b7-832428d2af34.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Twitter alerts",
"description": "Track Twitter mentions and be alerted via email",
"val": "@stevekrouse.twitterAlert",
image:
"https://air-prod.imgix.net/4c81af31-0832-4c72-8afb-34ef23fa03c9.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Air quality",
"description":
"Get email alerts when the air quality is bad. Val created with much help from @russbiggs (Director of Technology at OpenAQ)",
"val": "@stevekrouse.aqi",
image:
"https://air-prod.imgix.net/1a21321c-2bb7-4aa8-8c26-eb58cf1787a2.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Annoy friends in Bluesky",
"description": "ChatGPT powered bot for the sole purpose of annoying friends on Bluesky.",
"val": "@ajax.annoy",
image:
"https://air-prod.imgix.net/d21d3c1f-99e9-4356-85ed-bc6d7746c867.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Venue calendar",
"description": "Get a venue calendar through Resy",
"val": "@rlesser.Resy_getVenueCalendar",
image:
"https://air-prod.imgix.net/cab68838-bc60-48a2-8b00-50f203ac303d.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
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 { renderToString } from "npm:react-dom/server";
export let welcomeEmail = renderToString(
<div style={{ maxWidth: "500px", fontFamily: "sans-serif" }}>
<h2>Welcome to Val Town!</h2>
<p>
Val Town is a social website to write and deploy Javascript functions. You can create APIs, crons, email yourself,
store data – all from the browser, and deployed in miliseconds.
</p>
<p>
<b>1. Learn how to val</b>
</p>
<p>
<a href="https://docs.val.town/">Read our docs</a>!{" "}
Learn to deploy vals, persist state, connect to APIs, and more.
</p>
<p>
<b>2. Join our Discord</b>
</p>
<p>
<a href="https://discord.gg/dHv45uN5RY">Join our Discord</a>{" "}
so you can ask questions, share your cool vals, and learn about the cutting-edge of Val Town. Don't be shy about
posting links to vals you want help debugging.
</p>
<p>
<b>3. Install some useful vals</b>
</p>
<ul>
<li>
<a href="https://www.val.town/v/stevekrouse/dailyDadJoke">Daily Dad Joke</a>
</li>
<li>
<a href="https://www.val.town/v/rodrigotello/hnFollow">Follow people on Hacker News</a>
</li>
<li>
Personalized weather alerts: <a href="https://www.val.town/v/stevekrouse/weatherGPT">GPT</a>,{" "}
<a href="https://www.val.town/v/stevekrouse/umbrellaReminder">rain</a>,{" "}
<a href="https://www.val.town/v/stevekrouse/aqi">air quality</a>
</li>
<li>
RSS notifications: <a href="https://www.val.town/v/stevekrouse/pollRSSFeeds">to your email</a>,{" "}
<a href="https://posthog.com/tutorials/rss-item-capture">to Posthog</a>,{" "}
<a href="https://www.val.town/v/jordan/bsky_rss_poll">to anywhere</a>
</li>
<li>
<a href="https://www.val.town/v/healeycodes/isMyWebsiteDown">Website downtime monitor</a>
</li>
<li>
<a href="https://www.val.town/v/stevekrouse/btcPriceAlert">BTC price alert</a>

Bluesky keyword alerts

Custom notifications for when you, your company, or anything you care about is mentioned on Bluesky.

1. Query

Specify your queries in the queries variable.

Bluesky doesn't support boolean OR yet so we do a separate search for each keyword.

2. Notification

Below I'm sending these mentions to a private channel in our company Discord, but you can customize that to whatever you want, @std/email, Slack, Telegram, whatever.

Readme
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
import { blob } from "https://esm.town/v/std/blob";
import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { searchBlueskyPosts } from "https://esm.town/v/stevekrouse/searchBlueskyPosts";
const encounteredIDs_KEY = "bluesky_encounteredIDs";
const queries = ["val town", "val.town"];
export const blueskyAlert = async () => {
let posts = (await Promise.all(queries.map(searchBlueskyPosts))).flat();
// filter for new posts
let encounteredIDs = await blob.getJSON(encounteredIDs_KEY) ?? [];
let newPosts = posts.filter((post) => !encounteredIDs.includes(post.tid));
await blob.setJSON(encounteredIDs_KEY, [
...encounteredIDs,
...newPosts.map((post) => post.tid),
]);
if (newPosts.length === 0) return;
// format
const content = posts.map(
post => `https://bsky.app/profile/${post.user.handle}/post/${post.tid.split("/")[1]}`,
).join(
"\n",
);
// notify
await discordWebhook({
url: Deno.env.get("mentionsDiscord"),
content,
});
return newPosts;
};

Twitter 𝕏 keyword Alerts

Custom notifications for when you, your company, or anything you care about is mentioned on Twitter.

1. Authentication

You'll need a Twitter Bearer Token. Follow these instructions to get one.

Unfortunately it costs $100 / month to have a Basic Twitter Developer account. If you subscribe to Val Town Pro, I can let you "borrow" my token. Just comment on this val and I'll hook you up.

2. Query

Change the query variable for what you want to get notified for.

You can use Twitter's search operators to customize your query, for some collection of keywords, filtering out others, and much more!

3. Notification

Below I'm sending these mentions to a private channel in our company Discord, but you can customize that to whatever you want, @std/email, Slack, Telegram, whatever.

Readme
Runs every 1 hrs
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
import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
import { twitterSearch } from "https://esm.town/v/stevekrouse/twitterSearch";
const query = "\"val.town\" OR \"val town\" -_ValTown_";
export async function twitterAlert({ lastRunAt }: Interval) {
const results = await twitterSearch({
query,
start_time: lastRunAt,
bearerToken: Deno.env.get("twitter"),
});
if (!results.length) return;
// format results
let content = results
.map(({ author_name, author_username, text, id }) => `https://twitter.com/${author_username}/status/${id}`)
.join("\n");
// notify
await discordWebhook({
url: Deno.env.get("mentionsDiscord"),
content,
});
}
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
import { slackrespond } from "https://esm.town/v/nate/slackrespond";
import process from "node:process";
// A Slack slash command app to evaluate a val.town expression.
//
// Setup: Add a slash command using
// "https://api.val.town/express/@nate.slackapp" as the request URL.
//
// Usage: /yourcommandname {expression}
//
// Examples:
// - /evaltown 1+1
// - /evaltown @nate.email("Hi from Slack!")
export let slackapp = async (req, res) => {
if ("ssl_check" in req.body) return;
if (
!("api_app_id" in req.body) ||
req.body.api_app_id !== process.env.SLACK_API_APP_ID ||
!("text" in req.body) ||
!("response_url" in req.body)
) {
res.status(200);
res.set("Content-Type", "application/json");
res.send(
JSON.stringify({
text: "A parameter was invalid or missing.",
})
);
return;
}
// Slack requires a response within 3 seconds so we send back an empty
// response as an acknowledgment and process the request asynchronously.
res.status(200).send();
await slackrespond(req.body.response_url, req.body.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
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
import process from "node:process";
export const slackReplyToMessage = async (
req: express.Request,
res: express.Response,
) => {
// Verify the request is genuine
if (req.body.token !== process.env.slackVerificationToken) {
return res.status(401);
}
// Respond to the initial challenge (when events are enabled)
if (req.body.challenge) {
return res.send({ challenge: req.body.challenge });
}
// Reply to app_mention events
if (req.body.event.type === "app_mention") {
// Note: `req.body.event` has information about the event
// like the sender and the message text
const result = await fetchJSON(
"https://slack.com/api/chat.postMessage",
{
headers: {
"Authorization": `Bearer ${process.env.slackToken}`,
},
method: "POST",
body: JSON.stringify({
channel: req.body.event.channel,
thread_ts: req.body.event.ts,
text: "Hello, ~World~ from Val Town!",
}),
},
);
// Slack replies with information about the created message
console.log(result);
}
};
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 valTownInspoList = [{
"title": "What can I make in Val Town?",
"description":
"The data for this page was created by using Val Town as a CMS",
"val": "@tmcw.valTownInspoList",
image:
"https://air-prod.imgix.net/abff23bf-fc18-485f-a6b5-4a1c8b7f91ec.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "NASA photo of the day.",
"description": "Returns NASA's Astronomy Picture of the Day (APOD)",
"val": "@rodrigotello.nasaAPOD",
"image":
"https://air-prod.imgix.net/11ab8b2e-c052-4574-8341-96d5d74040cc.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "hnFollow",
"description":
"Get email notifications any time an author you follow posts in Hacker News.",
"val": "@rodrigotello.hnFollow",
"image":
"https://air-prod.imgix.net/61e08fed-c77e-4a7b-9ae5-1efabb349574.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "GitHub events",
"description": "Get a GitHub users' public events",
"val": "@stevekrouse.githubEvents",
image:
"https://air-prod.imgix.net/3545d2cb-7cae-49d9-98b7-832428d2af34.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Twitter alerts",
"description": "Track Twitter mentions and be alerted via email",
"val": "@stevekrouse.twitterAlert",
image:
"https://air-prod.imgix.net/4c81af31-0832-4c72-8afb-34ef23fa03c9.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Air quality",
"description":
"Get email alerts when the air quality is bad. Val created with much help from @russbiggs (Director of Technology at OpenAQ)",
"val": "@stevekrouse.aqi",
image:
"https://air-prod.imgix.net/1a21321c-2bb7-4aa8-8c26-eb58cf1787a2.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Annoy friends in Bluesky",
"description":
"ChatGPT powered bot for the sole purpose of annoying friends on Bluesky.",
"val": "@ajax.annoy",
image:
"https://air-prod.imgix.net/d21d3c1f-99e9-4356-85ed-bc6d7746c867.jpg?w=1200&h=2000&auto=compress&ixlib=react-9.5.4",
}, {
"title": "Venue calendar",
"description": "Get a venue calendar through Resy",
"val": "@rlesser.Resy_getVenueCalendar",
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 { dictionary } from "https://esm.town/v/fields/dictionary";
export let defineSlackApp = async (req: express.Request, res: express.Response) => {
const text = req.body.text.trim();
res.status(200);
// /undefine
if (req.body.command == "/undefine") {
if (dictionary[text.toLowerCase()]) {
delete dictionary[text.toLowerCase()];
res.send(`Removed definition for *${text}*`);
}
res.send(`There is no definition for *${text}*`);
return;
}
// /define
// parse args
let word;
let definition;
if (!text.includes('"')) {
word = text;
}
else {
const quote = text.indexOf('"');
word = text.slice(0, quote).trim();
definition = text.slice(quote + 1).replace('"', "");
}
if (definition) {
// Set definition
dictionary[word.toLowerCase()] = {
word: word,
definition: definition,
};
res.send(`*${word}*: ${definition}`);
return;
}
// Get definition
// Check for exact match
const exactMatch = dictionary[word.toLowerCase()];
if (exactMatch) {
res.send(`${exactMatch.word}: _${exactMatch.definition}_`);
return;
}
// Check for what they might have meant
const { default: didYouMean, ReturnTypeEnums, ThresholdTypeEnums } =
await import("npm:didyoumean2");
const fuzzyFind = didYouMean(
word.toLowerCase(),
Object.values(dictionary),
{
matchPath: ["word"],