Avatar

@iakovos

12 public vals
Joined August 14, 2023
1
2
3
4
5
6
7
8
9
10
11
12
13
const getMediaAttributeUrl = (
attribute: MediaAttribute | MediaAttribute[],
): string | null => {
if (Array.isArray(attribute)) {
return attribute[0]?._attributes?.url ?? null;
}
return attribute?._attributes?.url ?? null;
};
interface MediaAttribute {
_attributes?: {
url?: string;
};
}
0
2
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
const fetchAndParseFeeds = async (url: string): Promise<FeedItem[]> => {
const { xml2js } = await import("https://deno.land/x/xml2js@1.0.0/mod.ts");
const { DOMParser } = await import(
"https://deno.land/x/deno_dom/deno-dom-wasm.ts"
);
const xml = await @me.fetchText(url);
const json = xml2js(xml, { compact: true });
const items = json.rss?.channel?.item || json.feed?.entry;
const parsedItems: FeedItem[] = items?.map((item) => {
const content = @me.extractContent(item);
const document = content &&
new DOMParser().parseFromString(content, "text/html");
const mediaThumbnail = @me.getMediaAttributeUrl(
item["media:thumbnail"],
);
const mediaContent = @me.getMediaAttributeUrl(
item["media:content"],
);
const paragraph =
document?.querySelector("p:not(.caption)")?.textContent?.trim() ?? "";
const imgTag = document?.querySelector("img");
const imgSrc = imgTag?.getAttribute("src");
const image = imgSrc || mediaThumbnail || mediaContent || null;
const link = @me.getLink(item);
const { _text, _cdata } = item.title ?? {};
const title = _text ?? _cdata ?? "";
const pubDate = item.pubDate?._text ?? item.updated?._text ?? "";
const description = paragraph || item.description?._text ||
item.description?._cdata || "";
return { title, description, image, pubDate, link };
}) ?? [];
return parsedItems;
};
interface FeedItem {
title: string;
description: string;
image: string | null;
pubDate: string;
link: string;
}
0
2
1
2
3
4
5
6
7
8
9
10
11
12
13
export const fetchText = async (url: string): Promise<string | null> => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.text();
}
catch (error) {
console.error("Failed to fetch feeds:", error);
return null;
}
};
0
4
1
2
3
4
5
6
7
export const newRSSItems = async ({ url, type, lastRunAt }) => {
const items = await @me.fetchAndParseFeeds(url);
return items?.filter(({ pubDate }) =>
lastRunAt && new Date(pubDate) > new Date(lastRunAt)
).map((item) => ({ ...item, type }));
};
// Forked from @stevekrouse.newRSSItems
0
6
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
function renderFeedsForEmail(feeds) {
let html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Today's Feeds</title>
<style type="text/css">
a:hover { text-decoration: underline !important; }
img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
table { border-collapse: collapse !important; }
@media only screen and (max-width:768px) {
.article-header {
width:100% !important
}
.image-container {
height:68px !important;
width:110px !important;
}
p {
display:none !important;
}
}
.image-container {
background-color:#E0E0E0;
border-radius:4px;
height:80px;
width:128px;
}
.image-container img {
border-radius: inherit;
display: block;
height:100%;
width:100%;
}
</style>
</head>
<body>
<div style="font-family: sans-serif; max-width:600px; margin:0 auto">
<h1 style="text-align:center; color:#333;">Today's Feeds</h1>
`;
feeds.forEach((feed) => {
html += `
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation" style="margin-bottom:24px;">
<tr>
<td style="padding: 0; vertical-align:top;">
<div class="image-container">
${
feed.image ? `<img src="${feed.image}" alt="Feed Image">` : ""
}
</div>
</td>
<td style="padding-left:1.2rem; vertical-align:top;">
<h2 class="article-header" style="margin-top:0; margin-bottom:4px; font-size:1rem; color:#333; width:70%; line-height:1.2;">
<a class="article-link" href="${feed.link}" style="text-decoration:none; color:#333;">${feed.title}</a>
</h2>
<div style="font-size:0.9em; color:#999;">
${feed.pubDate} |
<a href="${feed.website}" style="color:#999; text-decoration:none;">${feed.website}</a>
</div>
<p style="margin-top:4px; color:#999;">${feed.description}</p>
</td>
</tr>
</table>
`;
});
html += `
</div>
</body>
</html>`;
return html;
}
0
6
1
2
3
4
5
6
7
8
9
function formatFeedDate(dateString: string): string {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
};
const date = new Date(dateString);
return date.toLocaleDateString(undefined, options);
}
0
4
1
2
3
4
5
6
7
8
export const getUrlHostname = (url: string): string => {
try {
return new URL(url).hostname;
}
catch {
return "";
}
};
0
3
1
2
// set by iakovos.pollRSSFeeds at 2023-09-23T07:08:50.944Z
let lastRunAt = 1695452929191;
0
6
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
function renderTopStories(stories: {
id: number;
title: string;
url: string;
time: number;
type: string;
score: number;
by: string;
descendants: number; // Number of comments
}[]): string {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hacker News</title>
</head>
<body style="font-family: Helvetica, sans-serif; background-color: #f4f4f4; padding: 20px;">
<div style="max-width: 600px; background-color: #ffffff; margin: 0 auto; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1);">
<h1 style="text-align: center; border-bottom: 1px solid #e4e4e4; padding-bottom: 5px; margin-bottom: 20px; font-size: 2em; color: #222;">Hacker News</h1>
<ul style="list-style-type: none; padding: 0;">
${
stories.map((story) => `
<li style="padding: 10px 0; border-bottom: 1px solid #e4e4e4;">
<a href="${story.url}" target="_blank" style="text-decoration: none; font-weight: bold; color: #0056b3; font-size: 1.3em;">${story.title}</a>
<div style="margin: 2px 0; color: #888;">
<strong>Score:</strong> ${story.score} |
<strong>By:</strong> ${story.by} |
<a href="https://news.ycombinator.com/item?id=${story.id}" target="_blank" style="color: #888; text-decoration: none;">${story.descendants} comments</a> |
<a href="${story.url}" target="_blank" style="color: #888; text-decoration: none;">${
@me.getUrlHostname(story.url)
}</a>
</div>
</li>
`).join("")
}
</ul>
</div>
</body>
</html>
`;
return html;
}
0
1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Get the title of the top story on Hacker News
export async function hnTopTenStories() {
const topStories: Number[] = await fetch(
"https://hacker-news.firebaseio.com/v0/beststories.json?print=pretty",
).then((res) => res.json());
const top10Stories = await Promise.all(
topStories.slice(0, 10).map(async (id) => {
return await @stevekrouse.fetchJSON(
`https://hacker-news.firebaseio.com/v0/item/${id}.json`,
);
}),
);
return top10Stories;
}
// forked from @healeycodes.hnTopStories
// Forked from @jamiedubs.hnTopStories
// Forked from @stevekrouse.hnTopStory
0
2