ramkarthik
Software engineer. He/Him. ManUtd fan. I build side projects (http://kramkarthik.com/projects). Recent project - https://dotnetjobs.co/.
12 public vals
Joined May 26, 2023
A minimal bookmarking tool
This allows you to bookmark links and view them later. Completely powered by ValTown and SQLite.
To set this up for yourself
- Fork the val
- From your ValTown settings page, add an environment variable named
bookmarks_client_id
and give it a value (you will be using this for saving) - Add another environment variable named
bookmarks_client_secret
and give it a value (you will also be using this for saving) - At first, the "bookmarks" table will not exist, so we need to save an article first, which will create the "bookmarks" table
- To do this, add a bookmarklet to your browser with this value (replace
BOOKMARKS-CLIENT-ID
andBOOKMARKS-CLIENT-SECRET
with the values you added to the environment variables, and replaceBOOKMARKS-URL
with your VAL's URL):
javascript:void(open('BOOKMARKS-URL/save?u='+encodeURIComponent(location.href)+'&t='+encodeURIComponent(document.title)+'&id=BOOKMARKS-CLIENT-ID&secret=BOOKMARKS-CLIENT-SECRET', 'Bookmark a link', 'width=400,height=450'))
- Click this bookmarklet to bookmark the URL of the current active tab
- Go to your VAL URL homepage to see the bookmark
Demo
Here are my bookmarks: https://ramkarthik-bookmark.web.val.run/
Note
Make sure you don't share bookmarks_client_id
and bookmarks_client_secret
. It is used for authentication before saving a bookmark.
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?v=4";
import { Hono } from "npm:hono@3";
const app = new Hono();
app.get("/", async (c) => {
var page = parseInt(c.req.query("page") || "0");
var offset = 0;
if (page && page > 0) {
offset = page * 10;
}
var bookmarks = await sqlite.execute({
sql: "select title, url from bookmarks order by created_at desc limit 10 offset :offset",
args: { offset: offset },
});
var totalBookmarkRows = await sqlite.execute("select count(1) from bookmarks order by created_at desc");
var totalBookmarks = parseInt(totalBookmarkRows.rows[0][0].toString());
var pagesCount = Math.floor(
((totalBookmarks % 10) == 0 && totalBookmarks / 10 > 0)
? ((totalBookmarks / 10) - 1)
: (totalBookmarks / 10),
);
var bookmarksList = "";
for (var i = 0; i < bookmarks.rows.length; i++) {
bookmarksList += "<p>" + (page * 10 + i + 1) + ". <a href=\"" + bookmarks.rows[i][1] + "\">" + bookmarks.rows[i][0]
+ "</a></p>";
}
var pagination = "<div style=\"flex-direction: row; width: 100%;justify-content: space-between;\">";
if (page > 0) {
pagination += "<a href=\"?page=" + (page - 1) + "\"> < prev </a>";
}
if (page < pagesCount) {
pagination += "<a href=\"?page=" + (page + 1) + "\"> next > </a>";
}
pagination += "</div>";
const html = `<html>
<head>
<title>My reading list</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
</head>
<body style="width:70%; margin-left:15%">
<h1>Reading list</h1>`
+ bookmarksList
+ pagination + `</body>
</html>`;
return new Response(
html,
{
headers: {
"Content-Type": "text/html",
},
},
);
});
app.get("/save", async (c) => {
const id = c.req.query("id");
const secret = c.req.query("secret");
const title = c.req.query("t");
const url = c.req.query("u");
if (!id && !secret) {
return c.text("Authentication details (ID/Secret) missing!");
}
if (id != Deno.env.get("bookmarks_client_id") || secret != Deno.env.get("bookmarks_client_secret")) {
return c.text("Unauthorized!");
}
if (!url) {
return c.text("URL missing!");
}
const create_table = await sqlite.execute(
"CREATE TABLE IF NOT EXISTS bookmarks (title TEXT, url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)",
);
const res = await sqlite.execute({
sql: `insert into bookmarks (title, url) values (:title, :url)`,
args: { title: title, url: url },
});
return c.text("Saved!");
});
app.get("/api/bookmarks", async (c) => {
var baseUrl = c.req.url;
var page = parseInt(c.req.query("page") || "0");
var offset = 0;
if (page && page > 0) {
offset = page * 10;
}
var bookmarks = await sqlite.execute({
sql: "select title, url from bookmarks order by created_at desc limit 10 offset :offset",
args: { offset: offset },
});
var totalBookmarkRows = await sqlite.execute("select count(1) from bookmarks order by created_at desc");
var totalBookmarks = parseInt(totalBookmarkRows.rows[0][0].toString());
var pagesCount = Math.floor(
((totalBookmarks % 10) == 0 && totalBookmarks / 10 > 0)
? ((totalBookmarks / 10) - 1)
: (totalBookmarks / 10),
);
let response = {
prev: page > 0 ? baseUrl + "?page=" + (page - 1) : null,
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 { prsEmail } from "https://esm.town/v/ramkarthik/prsEmail";
import { reposStaleGithubPRs } from "https://esm.town/v/ramkarthik/reposStaleGithubPRs";
import process from "node:process";
// Sends an email with all the open PRs that have not had any activity
// for specified period of days
// Set up this function to run every day (or any interval)
export async function staleGithubPRsEmail() {
// Set up a secret named githubRepos with all the repos you want to analyze (comma separated)
let repos = process.env.githubRepos.split(",").filter((r) =>
r.trim()
);
let staleSinceDaysAgo = 3; // Any PR with no activity for this many days will qualify
let pullRequests = await reposStaleGithubPRs(
repos,
process.env.githubOwner, // Set up a secret named githubOwner with the owner name
process.env.github, // Create GitHub token and set up this secret
staleSinceDaysAgo,
);
let { html, subject } = prsEmail(
pullRequests,
staleSinceDaysAgo,
);
console.email({ html: html, subject: subject });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { bookmarksHtml } from "https://esm.town/v/ramkarthik/bookmarksHtml";
import process from "node:process";
import { raindropBookmarksSinceLastRun } from "https://esm.town/v/ramkarthik/raindropBookmarksSinceLastRun";
import { daysAgoFromToday } from "https://esm.town/v/ramkarthik/daysAgoFromToday";
export async function raindropBookmarksToEmail(interval: Interval) {
let lastRunAt = interval.lastRunAt?.toISOString() ||
daysAgoFromToday(7);
let bookmarks = await raindropBookmarksSinceLastRun(
lastRunAt,
process.env.raindrop,
);
if (bookmarks && bookmarks.length > 0) {
let emailHtml = bookmarksHtml(bookmarks);
console.email({
html: emailHtml,
subject: "[Raindrop] Your bookmarks since " + lastRunAt.split("T")[0],
});
}
}
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
export const prsEmail = (pullRequests: any, staleSinceDaysAgo: number) => {
let emailHtml = "";
let header =
`<h3>Below PRs have been stale for over ${staleSinceDaysAgo} days:</h3>`;
let footer =
`<p>Email from val.town. You can unsubscribe by clearing your interval here: https://val.town/@me.intervals</p>`;
emailHtml = header;
emailHtml += "<br />";
pullRequests.forEach((p, index) => {
emailHtml += `<b>Repo: ${p.repo}</b><ol>`;
p.prs.forEach((pr, index) => {
emailHtml += `<li><a href='${pr.url}'>${pr.title}</a>
<p>Created: ${pr.created_at}</p>
<p>Updated: ${pr.updated_at}</p>
<p>Author: ${pr.author}</p>
<p>Reviewers: ${pr.reviewers.join(", ")}</p>
<p>Head: ${pr.head}</p>
<p>Base: ${pr.base}</p>
</li>`;
});
emailHtml += "</ol><br /><br />";
});
emailHtml += footer;
let numberOfRepos = pullRequests.length > 3 ? 3 : pullRequests.length;
let topRepos = Array.from(new Set(pullRequests.map((p) => p.repo))).slice(
0,
numberOfRepos,
);
return {
html: emailHtml,
subject: "[GitHub] Stale PRs from " + topRepos.join(", "),
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { staleGithubPRs } from "https://esm.town/v/ramkarthik/staleGithubPRs";
export const reposStaleGithubPRs = async (
repos: string[],
owner: string,
token: string,
staleSinceDaysAgo: number,
) => {
let pullRequests = [];
for (const repo of repos) {
let prs = await staleGithubPRs(
owner,
repo,
token,
staleSinceDaysAgo,
);
if (prs && prs.length > 0) {
pullRequests.push({ repo: repo, prs: prs });
}
}
return pullRequests;
};
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 { daysAgoFromToday } from "https://esm.town/v/ramkarthik/daysAgoFromToday";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
export const staleGithubPRs = async (
owner: string,
repo: string,
token: string,
staleSinceDaysAgo: number,
) => {
let options = {
"headers": {
"Authorization": "Bearer " + token,
"X-GitHub-Api-Version": "2022-11-28",
},
};
let data = await fetchJSON(
`https://api.github.com/repos/${owner}/${repo}/pulls`,
options,
);
let pullRequests = [];
let staleDate = daysAgoFromToday(staleSinceDaysAgo);
if (data && data.length > 0) {
let prs = data?.filter((d) => d.updated_at <= staleDate).map((d) => {
return {
url: d.html_url,
title: d.title,
author: d.user.login,
created_at: d.created_at,
updated_at: d.updated_at,
reviewers: d.requested_reviewers.map((r) => r.login),
head: d.head.ref,
base: d.base.ref,
};
});
pullRequests.push(...prs);
}
return pullRequests;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const bookmarksHtml = (bookmarks: any[], header: string = "") => {
let emailHtml = "";
header = header || `<h3>Here are your bookmarks from Raindrop:</h3>`;
let footer =
`<p>Email from val.town. You can unsubscribe by clearing your interval here: https://val.town/@me.intervals</p>`;
emailHtml = header;
emailHtml += "<br /><ol>";
bookmarks.forEach((i, index) => {
emailHtml +=
`<li><a href='${i.link}'>${i.title}</a><p>${i.excerpt}</p></li>`;
});
emailHtml += "</ol>";
emailHtml += footer;
return emailHtml;
};
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 { raindropBookmarks } from "https://esm.town/v/ramkarthik/raindropBookmarks";
export const raindropBookmarksSinceLastRun = async (
lastRunAt: String,
raindropToken: String,
) => {
let bookmarks = [], reachedLastItem = false, page = 0;
while (!reachedLastItem) {
let data = await raindropBookmarks(
page,
raindropToken,
);
if (data.result && data.items.length > 0) {
data.items.every((item, index) => {
if (item.created >= lastRunAt) {
bookmarks.push(item);
return true;
}
else {
reachedLastItem = true;
return false;
}
});
}
else {
reachedLastItem = true;
}
page += 1;
}
return bookmarks;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
export const raindropBookmarks = async (
page: Number,
raindropToken: String,
) => {
let options = {
"headers": { "Authorization": "Bearer " + raindropToken },
};
let data = await fetchJSON(
"https://api.raindrop.io/rest/v1/raindrops/0?perpage=20&page=" + page,
options,
);
return data;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export async function domainAvailability(req, res) {
const whoiser = await import("npm:whoiser");
var domain = req.body;
let domainWhois = await whoiser.domain(domain);
console.log(domainWhois);
if (
(domainWhois["whois.verisign-grs.com"] &&
domainWhois["whois.verisign-grs.com"]["Domain Status"] &&
domainWhois["whois.verisign-grs.com"]["Domain Status"].length > 0) ||
(domainWhois["whois.nic.google"] &&
domainWhois["whois.nic.google"]["Domain Status"] &&
domainWhois["whois.nic.google"]["Domain Status"].length > 0)
)
res.send({ domain: domain, available: false });
else
res.send({ domain: domain, available: true });
}