Readme

bytes.dev newsletter notifier

hld architecture diagram for notifier

Tech Stack

How it works

At the lowest level it is powered by 3 main scripts, which are invoked by a scheduled cron job that runs daily

  • scraper Goes to bytes.dev and scrapes latest published newsletter
  • inserter Insert it to SQLite if this newsletter already not exists
  • notifier Uses Pushover API to send ios mobile notifications

Pushover notifications

image

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
import pushover from "https://esm.town/v/pranjaldotdev/pushover";
import { webScrapeBytesNewsletter } from "https://esm.town/v/pranjaldotdev/scraper";
import { sqlite } from "https://esm.town/v/std/sqlite";
// formats date in SQLITE format YYYY-MM-DD
function formatDate(date: string) {
let [month, day, year] = date.split("/");
if (month.length == 1) month = "0" + month;
if (day.length == 1) day = "0" + day;
return `${year}-${month}-${day}`;
}
// insert newsletter metadata in sqlite
async function insertRow(articleNumber: number, title: string, date: string) {
try {
await sqlite.execute({
sql: `insert into newsletter(article_number, title, date) values (:articleNumber, :title, :date)`,
args: { articleNumber, title, date: formatDate(date) },
});
} catch (err) {
console.error(err);
}
}
// check if newsletter id exists
async function checkNewsletterPresent(articleNumber: number) {
const data = await sqlite.execute({
sql: `SELECT EXISTS(SELECT 1 FROM newsletter WHERE article_number=:articleNumber)`,
args: { articleNumber },
});
return data.rows.length === 1;
}
// cron scheduled
export const scheduledNotifier = async (interval: Interval) => {
try {
const data = await webScrapeBytesNewsletter();
const isPresent = await checkNewsletterPresent(data.id);
let title = "";
if (isPresent) {
console.log(`Article ${data.id} already exists!!!`);
title = "Have you still read this amazing bytes.dev newsletter";
} else {
// insert and notify
await insertRow(data.id, data.title, data.date);
title = `Latest bytes.dev newsletter dropped ${data.date}`;
}
const response = await pushover({
token: Deno.env.get("PO_API_TOKEN")!,
user: Deno.env.get("PO_USER_KEY")!,
title,
message: data.title,
url: `https://bytes.dev/archives/${data.id}`,
});
console.log("Notified: ", response);
} catch (err) {
console.error("Error scraping newsletter ", err);
}
};
👆 This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data — all from the browser.