Avatar

@chet

3 likes22 public vals
Joined July 18, 2022
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
// NOTE: This doesn't work great.
const valDomain = "chet-notionSiteProxy.web.val.run";
const notionPage = "https://chetcorcos.notion.site/0e27612403084b2fb4a3166edafd623a";
export default async function(req: Request): Promise<Response> {
const notionUrl = new URL(notionPage);
const notionDomain = notionUrl.host;
const url = new URL(req.url);
if (url.pathname === "/") {
url.host = valDomain;
url.pathname = notionUrl.pathname;
return Response.redirect(url);
}
url.host = notionDomain;
const response = await fetch(url, req);
const contents = await response.text();
const fixed = contents
.split(notionDomain)
.join(valDomain)
.split("notion.so")
.join("val.run")
.split("notion.site")
.join("val.run");
const headers = new Headers(response.headers);
headers.delete("Content-Security-Policy");
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, HEAD, POST,PUT, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type");
const proxyRepsonse = new Response(fixed, { ...response, headers });
return proxyRepsonse;
}
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { fetch } from "https://esm.town/v/std/fetch";
import process from "node:process";
/** Must set process.env.WEATHER_API_KEY for weatherapi.com **/
export async function getWeather(
query: string,
days = 10,
): Promise<WeatherApiResponse> {
const weatherApiKey = process.env.WEATHER_API_KEY;
const q = encodeURIComponent(query);
const apiUrl = `https://api.weatherapi.com/v1/forecast.json?key=${weatherApiKey}&q=${q}&days=${days}`;
const response = await fetch(apiUrl);
const data: any = await response.json();
return data;
}
type WeatherApiResponse = {
location: {
name: string;
region: string;
country: string;
lat: number;
lon: number;
tz_id: string;
localtime_epoch: number;
localtime: string;
};
current: {
last_updated_epoch: number;
last_updated: string;
temp_c: number;
temp_f: number;
is_day: number;
condition: {
text: string;
icon: string;
code: number;
};
wind_mph: number;
wind_kph: number;
wind_degree: number;
wind_dir: string;
pressure_mb: number;
pressure_in: number;
precip_mm: number;
precip_in: number;
humidity: number;
cloud: number;
feelslike_c: number;
feelslike_f: number;
vis_km: number;
vis_miles: number;
uv: number;
gust_mph: number;
gust_kph: number;
};
forecast: {
forecastday: {
date: string;
date_epoch: number;
day: {
maxtemp_c: number;
maxtemp_f: number;
mintemp_c: number;
mintemp_f: number;
avgtemp_c: number;
avgtemp_f: number;
maxwind_mph: number;
maxwind_kph: number;
totalprecip_mm: number;
totalprecip_in: number;
totalsnow_cm: number;
avgvis_km: number;
avgvis_miles: number;
avghumidity: number;
daily_will_it_rain: number;
daily_chance_of_rain: number;
daily_will_it_snow: number;
daily_chance_of_snow: number;
condition: {
text: string;
icon: string;
code: number;
};
uv: number;
};
astro: {
sunrise: string;
sunset: string;
moonrise: string;
moonset: string;
moon_phase: string;
moon_illumination: string;
is_moon_up: number;
is_sun_up: number;
};
hour: {
time_epoch: number;
time: string;
temp_c: number;
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
import { blob } from "https://esm.town/v/std/blob?v=11";
import { email } from "https://esm.town/v/std/email?v=11";
import { fetchText } from "https://esm.town/v/stevekrouse/fetchText?v=6";
import { JSDOM } from "npm:jsdom";
const username = "ccorcos";
export default async function(interval: Interval) {
const url = "https://news.ycombinator.com/threads?id=" + username;
const html = await fetchText(url);
console.log("html", html.slice(0, 100));
const { document } = (new JSDOM(html)).window;
const comments = Array.from(document.querySelectorAll(".athing.comtr")) as any[];
const currentIds = new Set(comments.map(x => x.id));
const newIds = new Set(comments.map(x => x.id));
const key = "hn2:" + username;
const prevIds = (await blob.getJSON(key)) || [] as string[];
for (const id of prevIds) newIds.delete(id);
if (newIds.size === 0) {
console.log("No new comments.");
return;
}
for (const id of Array.from(newIds)) prevIds.push(id)
console.log(newIds.size + " new comments.");
await blob.setJSON(key, prevIds);
const newComments = comments.filter(comment => newIds.has(comment.id))
.map(comment => comment.textContent);
await email({
subject: "New HN comments",
text: url + "\n\n" + newComments.join("\n---\n"),
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import process from "node:process";
import { createJTW, getForecast } from "npm:apple-weatherkit";
export async function getAppleWeather(args: { lat: number; lng: number }) {
const token = createJTW({
teamId: process.env.APPLE_WEATHERKIT_TEAM_ID,
serviceId: process.env.APPLE_WEATHERKIT_SERVICE_ID,
keyId: process.env.APPLE_WEATHERKIT_KEY_ID,
privateKey: process.env.APPLE_WEATHERKIT_WEATHERKIT_KEY,
expireAfter: 60, // 1 minute
});
const forecast = await getForecast({ lat: args.lat, lng: args.lng, token });
return forecast;
}
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 { ValTupleStorage } from "https://esm.town/v/chet/ValTupleStorage";
const db = new ValTupleStorage("email");
export async function generateEmailKey() {
const key = Math.random().toString().slice(3);
const SecondMs = 1000;
const MinuteMs = 60 * SecondMs;
const HourMs = 60 * MinuteMs;
const DayMs = 24 * HourMs;
const expires = Date.now() + 2 * 365 * DayMs;
await db.write({ set: [{ key: [key], value: expires }] });
console.log("Generated a new key", key);
}
export async function listEmailKeys() {
const emailKeys = await db.scan();
console.log("Email keys:", emailKeys);
}
export async function expireEmailKey(key: string) {
const expires = Date.now() - 1;
await db.write({ set: [{ key: [key], value: expires }] });
}
export async function authenticateEmailKey(key: string) {
const result = await db.scan({ gte: [key], lte: [key], limit: 1 });
if (result.length === 0) return "Invalid key.";
const expires = result[0].value;
if (Date.now() >= expires) return "Key has expired.";
}
export async function resetEmailKeys() {
await db.destroy();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ValTupleStorage } from "https://esm.town/v/chet/ValTupleStorage";
const db = new ValTupleStorage("data");
await db.write({
set: [
{ key: ["names", "Chet"], value: null },
{ key: ["names", "Sam"], value: null },
{ key: ["names", "Steve"], value: null },
{ key: ["names", "Ivan"], value: null },
{ key: ["names", "Sergey"], value: null },
],
});
const result = await db.scan({ gte: ["names", "Sam"], limit: 2 });
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { sqlite } from "https://esm.town/v/std/sqlite";
import { jsonCodec } from "npm:lexicodec";
import { KeyValuePair, ScanStorageArgs, Tuple, WriteOps } from "npm:tuple-database";
export class ValTupleStorage {
ready: Promise<void>;
private async init() {
await sqlite.execute(`
create table if not exists ${this.tableName} (
key text primary key,
value text
)`);
}
constructor(public tableName: string) {
this.ready = this.init();
}
async scan(args: ScanStorageArgs = {}) {
await this.ready;
// Bounds.
let start = args.gte ? jsonCodec.encode(args.gte) : undefined;
let startAfter: string | undefined = args.gt
? jsonCodec.encode(args.gt)
: undefined;
let end: string | undefined = args.lte ? jsonCodec.encode(args.lte) : undefined;
let endBefore: string | undefined = args.lt
? jsonCodec.encode(args.lt)
: undefined;
const sqlArgs = {
start,
startAfter,
end,
endBefore,
limit: args.limit,
};
const where = [
start ? "key >= :start" : undefined,
startAfter ? "key > :startAfter" : undefined,
end ? "key <= :end" : undefined,
endBefore ? "key < :endBefore" : undefined,
]
.filter(Boolean)
.join(" and ");
let sqlQuery = `select * from ${this.tableName}`;
if (where) {
sqlQuery += " where ";
sqlQuery += where;
}
sqlQuery += " order by key";
if (args.reverse) {
sqlQuery += " desc";
}
if (args.limit) {
sqlQuery += ` limit :limit`;
}
const results = await sqlite.execute({
sql: sqlQuery,
args: sqlArgs,
});
const rows = results.rows.map((row: any) => {
const obj: any = {};
row.forEach((value, index) => {
obj[results.columns[index]] = value;
});
return obj;
});
return rows.map(
({ key, value }) => ({
key: jsonCodec.decode(key) as Tuple,
value: JSON.parse(value),
} as KeyValuePair),
);
}
async write(writes: WriteOps) {
await this.ready;
const batch: any[] = [];
for (const { key, value } of writes.set || []) {
batch.push({
sql: `insert or replace into ${this.tableName} values (:key, :value)`,
args: {
key: jsonCodec.encode(key),
value: JSON.stringify(value),
},
});
}
for (const tuple of writes.remove || []) {
batch.push({
sql: `delete from ${this.tableName} where key = :key`,
args: { key: jsonCodec.encode(tuple) },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { authenticateEmailKey } from "https://esm.town/v/chet/EmailKeys";
import { ValTupleStorage } from "https://esm.town/v/chet/ValTupleStorage";
import { email } from "https://esm.town/v/std/email?v=11";
export default async function(req: Request): Promise<Response> {
const searchParams = new URL(req.url).searchParams;
const { key, subject, text } = Object.fromEntries(searchParams.entries());
if (!key) return Response.json({ error: "Missing key query param." });
// if (!subject) return Response.json({ error: "Missing subject query param." });
// if (!text) return Response.json({ error: "Missing text query param." });
const error = await authenticateEmailKey(key);
if (error) return Response.json({ error });
await email({ subject, text });
return Response.json({ message: "Email sent!" });
}
1
2
3
4
5
6
7
8
import { powderNotify } from "https://esm.town/v/chet/powderNotify";
export default async function(interval: Interval) {
await Promise.all([
powderNotify("Truckee, CA"),
powderNotify("Kirkwood, CA"),
]);
}
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 { getWeather } from "https://esm.town/v/chet/getWeather";
import { email } from "https://esm.town/v/std/email?v=11";
import process from "node:process";
const toInches = (cm: number) => Math.round(cm * 2.54 * 10) / 10;
/** This doesnt work well */
export async function powderNotify(location: string) {
const weather = await getWeather(location);
const snowDays = weather.forecast.forecastday.filter(day => {
if (day.day.daily_chance_of_snow < 50) return false;
if (day.day.totalsnow_cm < 3) return false;
return true;
});
if (snowDays.length === 0) {
console.log("No upcoming snow days in " + location);
return;
}
console.log(snowDays.length + " upcoming snow days in " + location);
const body = snowDays
.map(day => {
const inches = toInches(day.day.totalsnow_cm);
return `- ${day.date}: ${inches}in snow, ${day.day.maxtemp_f}°F temp, ${day.day.maxwind_mph}mph wind`;
})
.join("\n");
const total = snowDays.map(day => toInches(day.day.totalsnow_cm)).reduce((sum, x) => sum + x, 0);
const subject = `${total}in of snow coming to ${location}`;
await email({ subject, text: body });
}