Avatar

@vtdocs

52 likes70 public vals
Joined June 12, 2023

Resy bot

This bot books restaurant reservations via Resy. Use it to snipe reservations at your favorite restaurant!

How to use it

Set up a scheduled val to call it like this:

Create valconst resyBotCron = async () => { const bookingInfo = await api(@vtdocs.resyBot, { slug: 'amaro-bar', city: 'ldn', day: '2023-07-05', start: '19:00', end: '21:00', partySize: 2, // Use https://www.val.town/settings/secrets for these! email: @me.secrets.resyEmail, password: @me.secrets.resyPassword, }); // If the val doesn't error, it successfully made a booking! // Send yourself an email like this: await @std.email({ text: bookingInfo, subject: 'resy bot made a booking for you!' }); }

How it works

This val makes the same requests that your browser would make when you reserve a slot on Resy (that's why it needs your login info – to request an auth token).

When there isn't a matching slot, this val errors and nothing else happens.

When a booking is available, this val books it and returns a description of the booking so you can email it to yourself (Resy will also email you).

This val will then stop attempting bookings for you until you change one of the arguments you're passing (it concats the non-sensitive arguments and uses this as a key).

Credit to @rlesser and @alp for their existing Resy vals (search for resy on here).

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { set } from "https://esm.town/v/vtdocs/set";
import { resyBookSlot } from "https://esm.town/v/vtdocs/resyBookSlot";
import { resyGetSlotBookingToken } from "https://esm.town/v/vtdocs/resyGetSlotBookingToken";
import { resyGetMatchingSlot } from "https://esm.town/v/vtdocs/resyGetMatchingSlot";
import { resyVenueIdFromSlugAndCity } from "https://esm.town/v/vtdocs/resyVenueIdFromSlugAndCity";
import { resyAuth } from "https://esm.town/v/vtdocs/resyAuth";
let { resyBotData } = await import("https://esm.town/v/vtdocs/resyBotData");
import { sha256 } from "https://esm.town/v/vtdocs/sha256";
export const resyBot = async (opts: {
slug: string; // amaro-bar
city: string; // ldn
day: string; // 2023-07-05
start: string; // 19:00
end: string; // 21:00
partySize: number; // 2
email: string; // resy email
password: string;
}): Promise<string> => {
const { slug, city, day, start, end, partySize, email, password } = opts;
// Avoid duplicate bookings by looking for a successful booking for the current parameters
const key = [
slug,
city,
day,
start,
end,
partySize,
await sha256(email),
].join(",");
if (resyBotData === undefined) {
resyBotData = {};
}
if (resyBotData[key] !== undefined) {
throw new Error(
`not running resy bot – successful booking exists for ${key}`,
);
}
const auth = await resyAuth(email, password);
const venue = await resyVenueIdFromSlugAndCity(
auth.token,
slug,
city,
);
// If there are no matching slots, an error is thrown
const matchingSlot = await resyGetMatchingSlot(
auth.token,
venue.id,
day,
start,
end,
partySize,
);
// At this point, there's a bookable slot (but it could still be sniped from us!)
const { bookToken } = await resyGetSlotBookingToken(
auth.token,
matchingSlot.config.token,
matchingSlot.date.start,
partySize,
);
if (auth.paymentMethods.length === 0) {
throw new Error("no payment methods on account (add one and try again)");
}
const bookingMetadata = await resyBookSlot(
auth.token,
bookToken,
auth.paymentMethods[0].id,
);
// Store successful booking to avoid duplicate bookings
resyBotData[key] = bookingMetadata;
await set("resyBotData", resyBotData);
return `Booked ${slug} in ${city} at ${matchingSlot.date.start} for ${partySize} people.
Check https://resy.com/account/reservations-and-notify for more details!`;
};

Guestbook Example

See @vtdocs.guestbook to set up your own interactive guestbook.

This is an example for testing!

Readme
1
2
3
4
5
import { generateGuestbookSnippet } from "https://esm.town/v/vtdocs/generateGuestbookSnippet";
export const guestbookExample = (req: express.Request, res: express.Response) => {
res.send(generateGuestbookSnippet("vtdocs", "guestbook"));
};

Guestbook

You can put an interactive guestbook on your website using vals!

This val is the backend of the guestbook that returns existing messages and handles new messages.

To generate a HTML snippet to post on your website, use @vtdocs.generateGuestbookSnippet.

Setup

Fork this val.

Paste the below snippet into your Val Town workspace – replacing alice and guestbook with your Val Town username and the name of @vtdocs.guestbook fork respectively.

@vtdocs.generateGuestbookSnippet('alice', 'guestbook')

Place the HTML block it returns anywhere on your website.

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
import { set } from "https://esm.town/v/std/set?v=11";
let { guestbookMessages } = await import("https://esm.town/v/vtdocs/guestbookMessages");
export const guestbook = async (req: express.Request, res: express.Response) => {
const esc = (await import("npm:escape-html@1.0.3")).default;
if (guestbookMessages === undefined) {
guestbookMessages = [];
}
if (req.method === "GET") {
return res.json(guestbookMessages);
}
if (req.body.name === undefined || req.body.text === undefined) {
return res.status(400);
}
guestbookMessages.push({
name: esc(req.body.name),
text: esc(req.body.text),
});
await set(
"guestbookMessages",
guestbookMessages,
);
return res.status(200);
};

A helper for writing parameterized Postgres queries for Supabase.

Part of the Supabase guide on docs.val.town.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const supaBaseQuery = async (
connectionString: string,
query: string,
args: any[] = [],
) => {
const { Client } = await import(
"https://deno.land/x/postgres@v0.17.0/mod.ts"
);
const client = new Client(connectionString);
await client.connect();
const result = await client.queryArray(query, args);
await client.end();
return 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
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
import process from "node:process";
export const browserlessPuppeteerExample = (async () => {
const { PuppeteerDeno } = await import(
"https://deno.land/x/puppeteer@16.2.0/src/deno/Puppeteer.ts"
);
const puppeteer = new PuppeteerDeno({
productName: "chrome",
});
const browser = await puppeteer.connect({
browserWSEndpoint:
`wss://chrome.browserless.io?token=${process.env.browserlessKey}`,
});
const page = await browser.newPage();
await page.goto("https://en.wikipedia.org/wiki/OpenAI");
const intro = await page.evaluate(
`document.querySelector('p:nth-of-type(2)').innerText`,
);
await browser.close();
return intro;
})();
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
import process from "node:process";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
export const browserlessScrapeExample = (async () => {
const res = await fetchJSON(
`https://chrome.browserless.io/scrape?token=${process.env.browserlessKey}`,
{
method: "POST",
body: JSON.stringify({
"url": "https://en.wikipedia.org/wiki/OpenAI",
"elements": [{
// The second <p> element on the page
"selector": "p:nth-of-type(2)",
}],
}),
},
);
// For this request, Browserless returns one data item
const data = res.data;
// That contains a single element
const elements = res.data[0].results;
// That we want to turn into its innerText value
const intro = elements[0].text;
return intro;
})();
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
export const generateInvoicePDF = async (options: {
invoiceNumber: string;
date: string;
customerName: string;
customerEmail: string;
items: {
description: string;
quantity: number;
price: number;
}[];
currencySymbol: string;
}): Promise<string> => {
const { jsPDF } = await import("npm:jspdf");
const doc = new jsPDF();
const {
invoiceNumber,
date,
customerName,
customerEmail,
items,
currencySymbol,
} = options;
// Set initial y position for content
let y = 20;
// Set invoice header
doc.setFontSize(18);
doc.text("Invoice", 105, y);
y += 10;
doc.setFontSize(12);
doc.text(`Invoice Number: ${invoiceNumber}`, 20, y);
doc.text(`Date: ${date}`, 150, y);
y += 10;
// Set customer information
doc.text(`Customer: ${customerName}`, 20, y);
doc.text(`Email: ${customerEmail}`, 150, y);
y += 10;
// Set table headers
doc.setFont(undefined, "bold");
doc.text("Description", 20, y);
doc.text("Quantity", 100, y);
doc.text("Price", 150, y);
y += 5;
// Draw table lines
doc.line(20, y, 180, y);
y += 5;
// Set table rows
doc.setFont(undefined, "normal");
items.forEach((item) => {
doc.text(item.description, 20, y);
doc.text(item.quantity.toString(), 100, y);
doc.text(item.price.toString(), 150, y);
y += 5;
});
// Draw table lines
doc.line(20, y, 180, y);
y += 10;
// Calculate total amount
const total = items.reduce(
(acc, item) => acc + item.quantity * item.price,
0,
);
// Set total amount
doc.setFont(undefined, "bold");
doc.text(`Total: ${total}`, 150, y);
return doc.output();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { telegramSendMessage } from "https://esm.town/v/vtdocs/telegramSendMessage";
import process from "node:process";
export const telegramWebhookEchoMessage = async (
req: express.Request,
res: express.Response,
) => {
// Verify this webhook came from our bot
if (
req.get("x-telegram-bot-api-secret-token") !==
process.env.telegramWebhookSecret
) {
return res.status(401);
}
// Echo back the user's message
const text: string = req.body.message.text;
const chatId: number = req.body.message.chat.id;
telegramSendMessage(
process.env.telegramBotToken,
{ chat_id: chatId, text },
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const reactSSRExample = async (req: express.Request, res: express.Response) => {
// Import React
const React = await import("npm:react");
const ReactDOMServer = await import("npm:react-dom/server");
// Define some components
function TodoItem(props) {
return React.createElement("li", null, props.text);
}
function TodoList(props) {
const todoItems = props.items.map((item, index) =>
React.createElement(TodoItem, { key: index, text: item })
);
return React.createElement("ul", null, todoItems);
}
const items = ["Buy groceries", "Do laundry", "Walk the dog"];
// Render the page
const html = ReactDOMServer.renderToString(
React.createElement(TodoList, { items: items }),
);
res.send(html);
};