Avatar

pranjaldotdev

9 public vals
Joined July 18, 2023

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);
}
};
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
import { fetch } from "https://esm.town/v/std/fetch";
// Send a pushover message.
// token, user, and other opts are as specified at https://pushover.net/api
export default async function pushover({
token,
user,
message,
title,
url,
}: {
token: string;
user: string;
message: string;
title: string;
url: string;
}) {
return await fetch("https://api.pushover.net/1/messages.json", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token,
user,
message,
title,
url,
}),
});
}
1
2
3
import { sqlite } from "https://esm.town/v/std/sqlite";
console.log(await sqlite.execute(`select * from newsletter`));
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
import cheerio from "npm:cheerio";
const NEWSLETTER_URL = "https://bytes.dev/archives";
function normalizeURL(url: string) {
return url.startsWith("http://") || url.startsWith("https://")
? url
: "http://" + url;
}
async function fetchText(url: string, options?: any) {
const response = await fetch(normalizeURL(url), {
redirect: "follow",
...(options || {}),
});
return response.text();
}
export const webScrapeBytesNewsletter = async () => {
const html = await fetchText(NEWSLETTER_URL);
const $ = cheerio.load(html);
const latestIssueSection = $("main > :nth-child(2)");
const title = latestIssueSection
.find("a")
.children()
.eq(1)
.find("h3")
.children()
.eq(1)
.text();
const articleNumber = latestIssueSection
.find("a")
.children()
.eq(1)
.find("h3")
.children()
.eq(0)
.text();
const date = latestIssueSection
.find("a")
.children()
.eq(1)
.find("div > div > span")
.text();
return {
id: Number(articleNumber.split(" ")[1]),
title,
date,
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import PubSub from "https://esm.town/v/pranjaldotdev/PubSub";
const pubsub = new PubSub();
const mySubscriber = (name: string) => {
console.log("Name logged is ", name);
};
const otherSubscriber = (name: string) => {
console.log("Other name is ", name);
};
const unsub = pubsub.subscribe("name", mySubscriber);
pubsub.publish("name", { name: "Pranjal" });
pubsub.subscribe("name", otherSubscriber);
pubsub.publish("name", "Scott Wu");
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
class PubSub {
private observers: {
[topic: string]: Array<(...args: unknown[]) => void>;
};
constructor() {
this.observers = {};
}
public publish(topic: string, ...args: unknown[]): void {
if (!this.observers[topic] || this.observers[topic].length === 0) {
throw new Error("No subscriber exists with this topic");
}
for (const callback of this.observers[topic]) {
callback(...args);
}
}
public subscribe(
topic: string,
callback: (...args: unknown[]) => void
): () => void {
if (!this.observers[topic]) this.observers[topic] = [];
this.observers[topic].push(callback);
return () => {
this.observers[topic] = this.observers[topic].filter(
(cb) => cb.toString() !== callback.toString()
);
console.log("Unsubscribed for topic: ", topic);
};
}
}
export default PubSub;
1
2
3
4
5
import { totalVotes } from "https://esm.town/v/pranjaldotdev/totalVotes";
import { set } from "https://esm.town/v/std/set?v=11";
export const incrementVotes = () =>
set("totalVotes", totalVotes + 1);
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
import { fetch } from "https://esm.town/v/std/fetch";
let { failureCount } = await import("https://esm.town/v/pranjaldotdev/failureCount");
let { successCount } = await import("https://esm.town/v/pranjaldotdev/successCount");
export async function pollVotingJob({ lastRunAt }: Interval) {
console.log("Last run was ", lastRunAt);
function sleep(ms: number) {
return new Promise((resolve) =>
setTimeout(() => {
console.log("Wait over");
resolve("ok");
}, ms)
);
}
async function doVote(): Promise<boolean> {
try {
const votingURL =
"https://jc-voting-prod.api.engageapps.jio/api/voting/questions/q-e4d1acfe-abd5-4417-a80b-1a9e0f54174a/answer";
const token =
"eyJhbGciOiJIUzI1NiJ9.eyJwbGF0Zm9ybSI6Imppb3Zvb3QiLCJ1c2VyaWR0eXBlIjoidXVpZCIsImlzSmlvVXNlciI6ZmFsc2UsImlzR3Vlc3QiOmZhbHNlLCJwaG9uZU5vIjoiMzM2NjBiNzQtM2U5YS00ZWJhLTlmMTMtNzBkNTQ3NmUwOTNkIiwicHJvZmlsZUlkIjoiMTE1ODc3YWQtZDE3Mi00YTAwLWI2OTMtMDlkZmNjNT
const response = await fetch(votingURL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: token,
"user-agent":
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
origin: "https://engage.jiocinema.com",
referer: "https://engage.jiocinema.com/",
},
body: JSON.stringify({
answer: ["alpha_Elvish Yadav"],
}),
});
const statusCode = response.status;
if (statusCode === 200) {
return true;
}
return false;
}
catch (ex) {
console.log("Error: ", ex);
return false;
}
}
async function fireRequests() {
console.log("Script booting up 🚀");
let successRequest = 0;
let failureRequest = 0;
for (let idx = 1; idx < 15000; idx++) {
if (idx % 100 === 0) {
await sleep(5000);
}
const isSuccess = await doVote();
if (isSuccess)
successCount += 1;
else
failureCount += 1;
}
return {
success: successCount,
failure: failureCount,
};
}
const response = await fireRequests();
await console.log("Completed stats: ", response);
}
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
import { fetch } from "https://esm.town/v/std/fetch";
let { failureCount } = await import("https://esm.town/v/pranjaldotdev/failureCount");
let { successCount } = await import("https://esm.town/v/pranjaldotdev/successCount");
export const sendBulkVote = (async () => {
const doVote = async (): Promise<boolean> => {
try {
const votingURL =
"https://jc-voting-prod.api.engageapps.jio/api/voting/questions/q-e4d1acfe-abd5-4417-a80b-1a9e0f54174a/answer";
const token =
"eyJhbGciOiJIUzI1NiJ9.eyJwbGF0Zm9ybSI6Imppb3Zvb3QiLCJ1c2VyaWR0eXBlIjoidXVpZCIsImlzSmlvVXNlciI6ZmFsc2UsImlzR3Vlc3QiOmZhbHNlLCJwaG9uZU5vIjoiMzM2NjBiNzQtM2U5YS00ZWJhLTlmMTMtNzBkNTQ3NmUwOTNkIiwicHJvZmlsZUlkIjoiMTE1ODc3YWQtZDE3Mi00YTAwLWI2OTMtMDlkZmNjNT
const response = await fetch(votingURL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: token,
"user-agent":
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
origin: "https://engage.jiocinema.com",
referer: "https://engage.jiocinema.com/",
},
body: JSON.stringify({
answer: ["alpha_Elvish Yadav"],
}),
});
const statusCode = response.status;
if (statusCode === 200) {
return true;
}
return false;
}
catch (ex) {
console.log("Error: ", ex);
return false;
}
};
const fireRequests = async () => {
console.log("Script booting up 🚀");
let successRequest = 0;
let failureRequest = 0;
for (let idx = 0; idx < 100; idx++) {
const isSuccess = await doVote();
if (isSuccess)
successCount += 1;
else
failureCount += 1;
}
};
await fireRequests();
})();
Next