CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/news/rss.xml.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import LRU from "lru-cache";
7
import { GetServerSideProps } from "next";
8
import { create as createXML } from "xmlbuilder2";
9
import type { XMLBuilder } from "xmlbuilder2/lib/interfaces";
10
11
// We cache the processed RSS feed for 10 minutes, so that we don't have to recompute it every time.
12
const cache = new LRU<Channel | "all", any>({
13
max: 10,
14
ttl: 10 * 60 * 1000,
15
});
16
17
import getCustomize from "@cocalc/database/settings/customize";
18
import { capitalize } from "@cocalc/util/misc";
19
import { slugURL } from "@cocalc/util/news";
20
import {
21
CHANNELS,
22
CHANNELS_DESCRIPTIONS,
23
Channel,
24
} from "@cocalc/util/types/news";
25
import { renderMarkdown } from "lib/news";
26
import { getFeedData } from "@cocalc/database/postgres/news";
27
28
// Empty page. getServerSideProps below defines what's going on
29
export default function RSS() {
30
return null;
31
}
32
33
// we have one RSS channel. this populates it with all entries from the database – with the given ordering
34
async function populateNewsItems(
35
xml: XMLBuilder,
36
ch: Channel | "all",
37
dns: string
38
): Promise<XMLBuilder> {
39
for (const n of await getFeedData()) {
40
const { id, text, title, date, channel } = n;
41
if (ch != "all" && channel !== ch) continue;
42
const pubDate: Date =
43
typeof date === "number" ? new Date(date * 1000) : date;
44
// URL visible to the user
45
const url = `https://${dns}/${slugURL(n)}`;
46
// GUID must be globally unique, not shown to USER
47
const guid = `https://${dns}/news/${id}`;
48
49
xml
50
.ele("item")
51
.ele("title")
52
.dat(title)
53
.up()
54
.ele("link")
55
.txt(url)
56
.up()
57
.ele("description")
58
.dat(renderMarkdown(text))
59
.up()
60
.ele("pubDate")
61
.txt(pubDate.toUTCString())
62
.up()
63
.ele("guid")
64
.txt(guid)
65
.up();
66
}
67
68
return xml;
69
}
70
71
// render RSS news feed
72
// check: https://validator.w3.org/feed/check.cgi
73
// Ref: https://www.w3.org/Protocols/rfc822/ (e.g. that's why it's date.toUTCString())
74
// There can only be one channel per RSS feed, but we let users filter by channel.
75
async function getXML(channel?: string): Promise<string> {
76
const { siteName, dns } = await getCustomize();
77
if (!dns) throw Error("no dns");
78
79
const ch: Channel | "all" =
80
channel && CHANNELS.includes(channel as Channel)
81
? (channel as Channel)
82
: "all";
83
84
const cached = cache.get(ch);
85
if (cached) return cached;
86
87
const selfLink = `https://${dns}/news/rss.xml`;
88
const atom = "http://www.w3.org/2005/Atom";
89
const descExtra = ch === "all" ? "" : `${CHANNELS_DESCRIPTIONS[ch]}. `;
90
91
const root = createXML({ version: "1.0", encoding: "UTF-8" })
92
.ele("rss", { version: "2.0" })
93
.att(atom, "xmlns:atom", atom);
94
95
const xml: XMLBuilder = root
96
.ele("channel")
97
.ele("atom:link", {
98
href: selfLink,
99
rel: "self",
100
type: "application/rss+xml",
101
})
102
.up()
103
.ele("title")
104
.txt(`${siteName} News${ch != "all" ? `– ${capitalize(ch)}` : ""}`)
105
.up()
106
.ele("description")
107
.txt(
108
`News about ${siteName}. ${descExtra}Also available at https://${dns}/news`
109
)
110
.up()
111
.ele("link")
112
.txt(selfLink)
113
.up()
114
.ele("pubDate")
115
.txt(new Date().toUTCString())
116
.up();
117
118
await populateNewsItems(xml, ch, dns);
119
120
const xmlstr = xml.end({ prettyPrint: true });
121
cache.set(ch, xmlstr);
122
return xmlstr;
123
}
124
125
export const getServerSideProps: GetServerSideProps = async ({
126
query,
127
res,
128
}) => {
129
if (!res) return { props: {} };
130
const { channel } = query;
131
const ch = typeof channel === "string" ? channel : undefined;
132
133
try {
134
res.setHeader("Content-Type", "text/xml");
135
res.setHeader("Cache-Control", "public, max-age=3600");
136
res.write(await getXML(ch));
137
res.end();
138
} catch (err) {
139
console.error(err);
140
res.statusCode = 500;
141
res.end();
142
}
143
144
return {
145
props: {},
146
};
147
};
148
149