Avatar

@andreterron

24 likes132 public vals
Joined August 18, 2022
Dev Tools • Quantified Self Exploring Social Programming at Val Town (This platform!)

Actually, It's 𝕏

An annoying bot that corrects people about the new name of the app previously known as Twitter. Follow it on 𝕏!

Screenshot 2023-08-17 at 4.48.03 PM.png

The Twitter → 𝕏 transition is pretty painful, and this bot is here to make things even worse!

Want to create your own bot? Check out https://www.val.town/v/andreterron.twitter

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
import { correctPostAboutTwitter } from "https://esm.town/v/andreterron/correctPostAboutTwitter";
import { sortedPosts } from "https://esm.town/v/andreterron/sortedPosts";
import { searchPostsForTheActuallyBot } from "https://esm.town/v/andreterron/searchPostsForTheActuallyBot";
import { refreshActuallyBotToken } from "https://esm.town/v/andreterron/refreshActuallyBotToken";
export async function actuallyItsXBot() {
const token = await refreshActuallyBotToken();
//
// Search
const posts = await searchPostsForTheActuallyBot(
token.access_token,
);
const detailed = await sortedPosts({
posts,
accessToken: token.access_token,
});
//
const post = detailed[0];
if (!post) {
if (posts.length) {
throw new Error("Failed to get post users");
}
console.log("No valid posts returned");
return;
}
const reply = await correctPostAboutTwitter({
id: post.id,
accessToken: token.access_token,
});
//
return reply;
}

Use GPT to generate vals on your account!

Describe the val that you need, call this function, and you'll get a new val on your workspace generated by OpenAI's API!

First, ensure you have a Val Town API Token, then call @andreterron.createGeneratedVal({...}) like this example:

@andreterron.createGeneratedVal({
  valTownKey: @me.secrets.vt_token,
  description:
    "A val that given a text file position in `{line, col}` and the text contents, returns the index position",
});

This will create a val in your workspace, and here's the one created by the example above: https://www.val.town/v/andreterron.getFileIndexPosition

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
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
import { runVal } from "https://esm.town/v/std/runVal";
export async function createGeneratedVal({ description, valTownKey }: {
description: string;
valTownKey: string;
}) {
const code = await runVal(
"andreterron.generateValCodeAPI",
description,
);
const val = await fetchJSON(
`https://api.val.town/v1/vals`,
{
method: "POST",
headers: {
Authorization: `Bearer ${valTownKey}`,
},
body: JSON.stringify({
code,
}),
},
);
return val;
}

Email notifications when your vals are referenced

Screenshot 2023-08-18 at 1.32.34 PM.png

Setup

  1. Create a Val Town API Token
  2. Add it to your secrets as valtown
  3. Fork & Run this val
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { refHtml } from "https://esm.town/v/andreterron/refHtml";
import process from "node:process";
import { references } from "https://esm.town/v/andreterron/references";
export const referencesNotify = async (interval: Interval) => {
const refs = await references({
token: process.env.valtown,
since: interval.lastRunAt,
});
if (refs?.length) {
console.email({
html: refHtml(refs),
}, "Val Town References");
}
return refs;
};

Login with Twitter 𝕏

Use this val to login with 𝕏 and save your token into a twitter_token val

Usage

  1. Fork this val
  2. Change the 🔒 privacy to "Unlisted"
  3. Navigate to https://developer.twitter.com/en/portal/dashboard to create an OAuth app and save your client id and secret to your secrets page
  4. Navigate to https://{your_username}-twitter.web.val.run to authenticate with 𝕏

And then you'll have your tokens saved to a twitter_token val!

Other Twitter 𝕏 helper 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
let { twitter_token } = await import("https://esm.town/v/andreterron/twitter_token");
let { twitter_auth_storage } = await import("https://esm.town/v/andreterron/twitter_auth_storage");
import process from "node:process";
import { twitterAuthHandler } from "https://esm.town/v/andreterron/twitterAuthHandler";
export async function twitter(req: Request) {
// Constants
const username = "YOUR_VALTOWN_USERNAME";
//
const { res, token, storage } = await twitterAuthHandler(req, {
client_id: process.env.twitter_client_id,
client_secret: process.env.twitter_client_secret,
redirect_uri: `https://${username}-twitter.web.val.run/callback`,
scopes: ["tweet.read", "tweet.write", "users.read", "offline.access"],
storage: twitter_auth_storage,
});
//
if (storage) {
twitter_auth_storage = storage;
}
//
if (token) {
twitter_token = token;
}
//
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
import { rootRef } from "https://esm.town/v/andreterron/rootRef?v=2";
import { ValRef } from "https://esm.town/v/andreterron/ValRef?v=1";
export function rootValRef(): ValRef | undefined {
const val = rootRef();
return val
? {
handle: val.userHandle,
name: val.valName,
}
: undefined;
}

Code on Val Town

Screenshot 2024-02-27 at 1.25.46 PM.png

Adds a "Code on Val Town" ribbon to your page. This lets your website visitors navigate to the code behind it.

This uses github-fork-ribbon-css under the hood.

Usage

Here are 2 different ways to add the "Code on Val Town" ribbon:

1. Wrap your fetch handler (recommended)

import { modifyFetchHandler } from "https://esm.town/v/andreterron/codeOnValTown?v=45";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default modifyFetchHandler(async (req: Request): Promise<Response> => {
  return html(`<h2>Hello world!</h2>`);
});

Example: @andreterron/openable_handler

2. Wrap your HTML string

import { modifyHtmlString } from "https://esm.town/v/andreterron/codeOnValTown?v=45";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default async (req: Request): Promise<Response> => {
  return html(modifyHtmlString(`<h2>Hello world!</h2>`));
};

Example: @andreterron/openable_html

Other ways

We made sure this was very modular, so you can also add the ribbon using these methods:

Customization

Linking to the val

These functions infer the val using the call stack or the request URL. If the inference isn't working, or if you want to ensure it links to a specific val, pass the val argument:

  • modifyFetchHandler(handler, {val: { handle: "andre", name: "foo" }})
  • modifyHtmlString("<html>...", {val: { handle: "andre", name: "foo" }})

Styling

You can set the style parameter to a css string to customize the ribbon. Check out github-fork-ribbon-css to learn more about how to style the element.

  • modifyFetchHandler(handler, {style: ".github-fork-ribbon:before { background-color: #333; }"})
  • modifyHtmlString("<html>...", {style: ".github-fork-ribbon:before { background-color: #333; }"})

Here's how you can hide the ribbon on small screens:

modifyFetchHandler(handler, {style: `@media (max-width: 768px) {
  .github-fork-ribbon {
    display: none !important;
  }
}`})

To-dos

  • Let users customize the ribbon. Some ideas are the text, color or placement.
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
import { modifyResponse } from "https://esm.town/v/andreterron/codeOnVT_modifyResponse?v=3";
import { ribbonElement } from "https://esm.town/v/andreterron/codeOnVT_ribbonElement?v=7";
import { inferRequestVal } from "https://esm.town/v/andreterron/inferRequestVal?v=2";
import { rootValRef } from "https://esm.town/v/andreterron/rootValRef?v=3";
import { ValRef } from "https://esm.town/v/andreterron/ValRef?v=1";
/**
* @param bodyText HTML string that will be used to inject the element.
* @param val Define which val should open. Defaults to the root reference.
*/
export function modifyHtmlString(
bodyText: string,
{ val, style }: { val?: ValRef; style?: string } = {},
) {
val = val?.handle && val?.name ? val : rootValRef();
if (!val) {
console.error("Failed to infer val. Please set the options.val parameter to the desired `{ handle, name }`");
return bodyText;
}
const element = ribbonElement({ val, style });
const bodyIndex = bodyText.lastIndexOf("</body>");
const htmlIndex = bodyText.lastIndexOf("</html>");
// Append the element before </body> or </html>. Get the last occurrence of each
// and use whichever one comes first.
if (bodyIndex !== -1 && (htmlIndex === -1 || bodyIndex < htmlIndex)) {
return bodyText.slice(0, bodyIndex) + element + bodyText.slice(bodyIndex);
} else if (htmlIndex !== -1) {
return bodyText.slice(0, htmlIndex) + element + bodyText.slice(htmlIndex);
} else {
return bodyText + element;
}
}
/**
* @param handler Fetch handler
* @param val Define which val should open
*/
export function modifyFetchHandler(
handler: (req: Request) => Response | Promise<Response>,
{ val, style }: { val?: ValRef; style?: string } = {},
): (req: Request) => Promise<Response> {
return async (req: Request): Promise<Response> => {
const res = await handler(req);
val = val?.handle && val?.name ? val : inferRequestVal(req);
return modifyResponse(res, { val, style });

"Code on Val Town" Ribbon HTML Element

Ribbon element used by @andreterron/codeOnValTown

Usage

  • ribbonElement({val: { handle: "andre", name: "foo" }}) - define which val to link to;
  • ribbonElement() - infer the val from the call stack.

Example: @andreterron/openable_element

import { ribbonElement } from "https://esm.town/v/andreterron/codeOnVT_ribbonElement?v=3";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default async (req: Request): Promise<Response> => {
  return html(`
    <h2>Hello world!</h2>
    <style>* { font-family: sans-serif }</style>
    ${ribbonElement()}
  `);
};
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
import { rootValRef } from "https://esm.town/v/andreterron/rootValRef?v=3";
import { ValRef } from "https://esm.town/v/andreterron/ValRef?v=1";
export function ribbonElement({ val, style }: { val?: ValRef; style?: string } = {}) {
const valRef = val?.handle && val?.name ? val : rootValRef();
if (!valRef) {
console.error("Failed to infer val. Please set the val parameter to the desired `{ handle, name }`");
return "";
}
const valSlug = `${valRef.handle}/${valRef.name}`;
return `
<div id="code-on-vt-host">
<template shadowrootmode="open">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"
/>
${style ? `<style>${style}</style>` : ""}
<a
href="https://www.val.town/v/${valSlug}"
rel="source"
target="_blank"
class="github-fork-ribbon"
data-ribbon="Code on Val Town"
title="Code on Val Town"
>
Code on Val Town
</a>
</template>
</div>`;
}
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 { basicAuthorization } from "https://esm.town/v/stevekrouse/basicAuthorization";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export async function refreshTwitterToken(
{ refresh_token, client_id, client_secret }: {
refresh_token: string;
client_id: string;
client_secret: string;
},
) {
const url = "https://api.twitter.com/2/oauth2/token";
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token,
client_id,
});
return fetchJSON(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
...basicAuthorization(client_id, client_secret),
},
body: body.toString(),
});
}

Receive form responses

Live demo: https://andreterron-form_demo.web.val.run/

Create a form that posts to your val:

<form action="https://andreterron-form_handler.web.val.run" method="post">
     <label for="username">Val Town username:</label>
     <input name="username" type="text">
     <button type="submit">Submit</button>
</form>

And get your results stored as an array:

// set by andreterron.form_handler at 2023-08-10T21:04:43.364Z
let formResponses = [{
  "username": "andreterron",
},
{
  "username": "stevekrouse",
}];

Usage

  1. Fork this val and click the 🔒 to set it as "Unlisted"
  2. Open the val menu → Endpoints → "Copy web endpoint"
  3. Use that url as the action attribute of your form

Check out the example val: https://www.val.town/v/andreterron.form_demo

Storage of form submissions

This val saves to another val (@me.formResponse), which has a 100kb limitation (250kb for pro users), if you want to store them in a more scalable solution, check out our guides

Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { html } from "https://esm.town/v/stevekrouse/html?v=5";
let { formResponses } = await import("https://esm.town/v/andreterron/formResponses");
export async function form_handler(req: Request) {
const data: any = Object.fromEntries((await req.formData()).entries());
//
formResponses = (formResponses ?? []).concat(
data,
);
console.email(data, "Form response received!");
//
return html(`<div
style="display:flex;
font-family: sans-serif;
width: 100vw;
height: 100vh;
justify-content: center;
align-items: center;
flex-direction: column;">
<span style="font-size: 32px; font-weight: bold;">Thanks for submitting the form!<span>
</div>`);
}

Generate a Val

Uses the OpenAI API to generate code for a val based on the description given by the user.

TODO:

  • Improve code detection on GPT response
  • Give more context on val town exclusive features like console.email or @references
  • Enable the AI to search val town to find other vals to use
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
import process from "node:process";
import { generateValCode } from "https://esm.town/v/andreterron/generateValCode";
export let genval = async (req: express.Request, res: express.Response) => {
const { default: htm } = await import("npm:htm");
const { default: vhtml } = await import("npm:vhtml");
const cookies = await import("https://deno.land/std@0.193.0/http/cookie.ts");
const html = htm.bind(vhtml);
const head = html`<head>
<title>Gen Val</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
</head>`;
const formClass =
"p-2 pt-16 group min-h-screen animate-all bg-emerald-50 w-full max-w-2xl mx-auto";
const inputClass = "p-2 border rounded w-full";
const labelClass =
"w-full text-sm font-bold uppercase text-emerald-800 [&>span]:pl-0.5 flex flex-col gap-2";
function parseCookies(cookie: string) {
const out: Record<string, string> = {};
const c = cookie.split(";");
for (const kv of c) {
const [cookieKey, ...cookieVal] = kv.split("=");
const key = cookieKey.trim();
out[key] = cookieVal.join("=");
}
return out;
}
switch (req.method) {
case "POST": {
const value = req.body;
if (
!("description" in value) ||
!value.description
) {
return res.end("Bad input");
}
const code = await generateValCode(
process.env.VT_OPENAI_KEY,
value.description,
);
const query = new URLSearchParams({ code });
const url = `https://www.val.town/embed/new?${query.toString()}`;
res.end(html`<html>
${head}
<body class="bg-emerald-50">
<div class="w-full max-w-2xl mx-auto flex flex-col gap-4 p-6 ">
<h1 class="text-3xl text-center font-bold text-emerald-800">